Updated and verified to work on CUDA 11.6

This commit is contained in:
Elijah Harmon 2023-02-28 15:58:28 -05:00
parent 307b05d238
commit a991acecb0
36 changed files with 1247 additions and 1020 deletions

View File

@ -100,7 +100,7 @@ If you are comfortable with your skills, you can run the other 4 versions. You c
#### **TensorRT Setup help** #### **TensorRT Setup help**
In our testing, the screenshot engine was the bottleneck. Tensorrt is only available via download from NVIDIA's site. In our testing, the screenshot engine was the bottleneck. Tensorrt is only available via download from NVIDIA's site.
You will need to make an account. Just go to this link and get `TensorRT 8.4 GA`. https://developer.nvidia.com/tensorrt You will need to install it via the .whl file they give you. You may also need https://developer.nvidia.com/cudnn. You will need to make an account. Just go to this link and get `TensorRT 8.5 GA Update 2`. https://developer.nvidia.com/tensorrt You will need to install it via the .whl file they give you. You may also need https://developer.nvidia.com/cudnn.
Sometimes you will need to remake the .engine model. To do this you need to visit the [YoloV5's Github repo](https://github.com/ultralytics/yolov5) and download it. Then execute the `export.py` script in the repo with the command below. This can take up to 20 minutes and have no visual feedback. It's not frozen, just looks like it. Sometimes you will need to remake the .engine model. To do this you need to visit the [YoloV5's Github repo](https://github.com/ultralytics/yolov5) and download it. Then execute the `export.py` script in the repo with the command below. This can take up to 20 minutes and have no visual feedback. It's not frozen, just looks like it.
@ -110,12 +110,14 @@ NOTE, you will need to use the provided or download a new version of the yolov5
### REQUIREMENTS ### REQUIREMENTS
- Nvidia RTX 2050 or higher - Nvidia RTX 2050 or higher
- Nvidia CUDA Toolkit 11.3 (https://developer.nvidia.com/cuda-11.3.0-download-archive) - One of the following
- Nvidia CUDA Toolkit 11.6 (Most compatibility) (https://developer.nvidia.com/cuda-11-6-0-download-archive)
- Nvidia CUDA Toolkit 11.7 (No onnx support currently, faster) (https://developer.nvidia.com/cuda-11-7-0-download-archive)
### Pre-setup ### Pre-setup
1. Unzip the file and place the folder somewhere easy to access 1. Unzip the file and place the folder somewhere easy to access
2. Make sure you have a pet Python (aka install python) - https://www.python.org/ 2. Make sure you have a pet Python (aka install python, use 3.10) - https://www.python.org/
***IF YOU GET THE FOLLOWING ERROR `python is not recognized as an internal or external command, operable program, or batch file` Watch This: https://youtu.be/E2HvWhhAW0g*** ***IF YOU GET THE FOLLOWING ERROR `python is not recognized as an internal or external command, operable program, or batch file` Watch This: https://youtu.be/E2HvWhhAW0g***
@ -123,7 +125,8 @@ NOTE, you will need to use the provided or download a new version of the yolov5
3. (Windows Users) Open up either `PowerShell` or `Command Prompt`. This can be done by pressing the Windows Key and searching for one of those applications. 3. (Windows Users) Open up either `PowerShell` or `Command Prompt`. This can be done by pressing the Windows Key and searching for one of those applications.
4. To install `PyTorch` go to this website, https://pytorch.org/get-started/locally/, and Select the stable build, your OS, Pip, Python and CUDA 11.3. Then select the text that is generated and run that command. 4. To install `PyTorch` go to this website, https://pytorch.org/get-started/locally/, and Select the stable build, your OS, Pip, Python and CUDA 11.6. Then select the text that is generated and run that command.
6. Copy and paste the commands below into your terminal. This will install the Open Source packages needed to run the program. You will need to `cd` into the downloaded directory first. Follow step 2 in the Run section below if you need help. 6. Copy and paste the commands below into your terminal. This will install the Open Source packages needed to run the program. You will need to `cd` into the downloaded directory first. Follow step 2 in the Run section below if you need help.
``` ```

672
export.py Normal file
View File

@ -0,0 +1,672 @@
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Export a YOLOv5 PyTorch model to other formats. TensorFlow exports authored by https://github.com/zldrobit
Format | `export.py --include` | Model
--- | --- | ---
PyTorch | - | yolov5s.pt
TorchScript | `torchscript` | yolov5s.torchscript
ONNX | `onnx` | yolov5s.onnx
OpenVINO | `openvino` | yolov5s_openvino_model/
TensorRT | `engine` | yolov5s.engine
CoreML | `coreml` | yolov5s.mlmodel
TensorFlow SavedModel | `saved_model` | yolov5s_saved_model/
TensorFlow GraphDef | `pb` | yolov5s.pb
TensorFlow Lite | `tflite` | yolov5s.tflite
TensorFlow Edge TPU | `edgetpu` | yolov5s_edgetpu.tflite
TensorFlow.js | `tfjs` | yolov5s_web_model/
PaddlePaddle | `paddle` | yolov5s_paddle_model/
Requirements:
$ pip install -r requirements.txt coremltools onnx onnx-simplifier onnxruntime openvino-dev tensorflow-cpu # CPU
$ pip install -r requirements.txt coremltools onnx onnx-simplifier onnxruntime-gpu openvino-dev tensorflow # GPU
Usage:
$ python export.py --weights yolov5s.pt --include torchscript onnx openvino engine coreml tflite ...
Inference:
$ python detect.py --weights yolov5s.pt # PyTorch
yolov5s.torchscript # TorchScript
yolov5s.onnx # ONNX Runtime or OpenCV DNN with --dnn
yolov5s_openvino_model # OpenVINO
yolov5s.engine # TensorRT
yolov5s.mlmodel # CoreML (macOS-only)
yolov5s_saved_model # TensorFlow SavedModel
yolov5s.pb # TensorFlow GraphDef
yolov5s.tflite # TensorFlow Lite
yolov5s_edgetpu.tflite # TensorFlow Edge TPU
yolov5s_paddle_model # PaddlePaddle
TensorFlow.js:
$ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
$ npm install
$ ln -s ../../yolov5/yolov5s_web_model public/yolov5s_web_model
$ npm start
"""
import argparse
import contextlib
import json
import os
import platform
import re
import subprocess
import sys
import time
import warnings
from pathlib import Path
import pandas as pd
import torch
from torch.utils.mobile_optimizer import optimize_for_mobile
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0] # YOLOv5 root directory
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
if platform.system() != 'Windows':
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from models.experimental import attempt_load
from models.yolo import ClassificationModel, Detect, DetectionModel, SegmentationModel
from utils.dataloaders import LoadImages
from utils.general import (LOGGER, Profile, check_dataset, check_img_size, check_requirements, check_version,
check_yaml, colorstr, file_size, get_default_args, print_args, url2file, yaml_save)
from utils.torch_utils import select_device, smart_inference_mode
MACOS = platform.system() == 'Darwin' # macOS environment
def export_formats():
# YOLOv5 export formats
x = [
['PyTorch', '-', '.pt', True, True],
['TorchScript', 'torchscript', '.torchscript', True, True],
['ONNX', 'onnx', '.onnx', True, True],
['OpenVINO', 'openvino', '_openvino_model', True, False],
['TensorRT', 'engine', '.engine', False, True],
['CoreML', 'coreml', '.mlmodel', True, False],
['TensorFlow SavedModel', 'saved_model', '_saved_model', True, True],
['TensorFlow GraphDef', 'pb', '.pb', True, True],
['TensorFlow Lite', 'tflite', '.tflite', True, False],
['TensorFlow Edge TPU', 'edgetpu', '_edgetpu.tflite', False, False],
['TensorFlow.js', 'tfjs', '_web_model', False, False],
['PaddlePaddle', 'paddle', '_paddle_model', True, True],]
return pd.DataFrame(x, columns=['Format', 'Argument', 'Suffix', 'CPU', 'GPU'])
def try_export(inner_func):
# YOLOv5 export decorator, i..e @try_export
inner_args = get_default_args(inner_func)
def outer_func(*args, **kwargs):
prefix = inner_args['prefix']
try:
with Profile() as dt:
f, model = inner_func(*args, **kwargs)
LOGGER.info(f'{prefix} export success ✅ {dt.t:.1f}s, saved as {f} ({file_size(f):.1f} MB)')
return f, model
except Exception as e:
LOGGER.info(f'{prefix} export failure ❌ {dt.t:.1f}s: {e}')
return None, None
return outer_func
@try_export
def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')):
# YOLOv5 TorchScript model export
LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...')
f = file.with_suffix('.torchscript')
ts = torch.jit.trace(model, im, strict=False)
d = {'shape': im.shape, 'stride': int(max(model.stride)), 'names': model.names}
extra_files = {'config.txt': json.dumps(d)} # torch._C.ExtraFilesMap()
if optimize: # https://pytorch.org/tutorials/recipes/mobile_interpreter.html
optimize_for_mobile(ts)._save_for_lite_interpreter(str(f), _extra_files=extra_files)
else:
ts.save(str(f), _extra_files=extra_files)
return f, None
@try_export
def export_onnx(model, im, file, opset, dynamic, simplify, prefix=colorstr('ONNX:')):
# YOLOv5 ONNX export
check_requirements('onnx>=1.12.0')
import onnx
LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...')
f = file.with_suffix('.onnx')
output_names = ['output0', 'output1'] if isinstance(model, SegmentationModel) else ['output0']
if dynamic:
dynamic = {'images': {0: 'batch', 2: 'height', 3: 'width'}} # shape(1,3,640,640)
if isinstance(model, SegmentationModel):
dynamic['output0'] = {0: 'batch', 1: 'anchors'} # shape(1,25200,85)
dynamic['output1'] = {0: 'batch', 2: 'mask_height', 3: 'mask_width'} # shape(1,32,160,160)
elif isinstance(model, DetectionModel):
dynamic['output0'] = {0: 'batch', 1: 'anchors'} # shape(1,25200,85)
torch.onnx.export(
model.cpu() if dynamic else model, # --dynamic only compatible with cpu
im.cpu() if dynamic else im,
f,
verbose=False,
opset_version=opset,
do_constant_folding=True, # WARNING: DNN inference with torch>=1.12 may require do_constant_folding=False
input_names=['images'],
output_names=output_names,
dynamic_axes=dynamic or None)
# Checks
model_onnx = onnx.load(f) # load onnx model
onnx.checker.check_model(model_onnx) # check onnx model
# Metadata
d = {'stride': int(max(model.stride)), 'names': model.names}
for k, v in d.items():
meta = model_onnx.metadata_props.add()
meta.key, meta.value = k, str(v)
onnx.save(model_onnx, f)
# Simplify
if simplify:
try:
cuda = torch.cuda.is_available()
check_requirements(('onnxruntime-gpu' if cuda else 'onnxruntime', 'onnx-simplifier>=0.4.1'))
import onnxsim
LOGGER.info(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...')
model_onnx, check = onnxsim.simplify(model_onnx)
assert check, 'assert check failed'
onnx.save(model_onnx, f)
except Exception as e:
LOGGER.info(f'{prefix} simplifier failure: {e}')
return f, model_onnx
@try_export
def export_openvino(file, metadata, half, prefix=colorstr('OpenVINO:')):
# YOLOv5 OpenVINO export
check_requirements('openvino-dev') # requires openvino-dev: https://pypi.org/project/openvino-dev/
import openvino.inference_engine as ie
LOGGER.info(f'\n{prefix} starting export with openvino {ie.__version__}...')
f = str(file).replace('.pt', f'_openvino_model{os.sep}')
args = [
'mo',
'--input_model',
str(file.with_suffix('.onnx')),
'--output_dir',
f,
'--data_type',
('FP16' if half else 'FP32'),]
subprocess.run(args, check=True, env=os.environ) # export
yaml_save(Path(f) / file.with_suffix('.yaml').name, metadata) # add metadata.yaml
return f, None
@try_export
def export_paddle(model, im, file, metadata, prefix=colorstr('PaddlePaddle:')):
# YOLOv5 Paddle export
check_requirements(('paddlepaddle', 'x2paddle'))
import x2paddle
from x2paddle.convert import pytorch2paddle
LOGGER.info(f'\n{prefix} starting export with X2Paddle {x2paddle.__version__}...')
f = str(file).replace('.pt', f'_paddle_model{os.sep}')
pytorch2paddle(module=model, save_dir=f, jit_type='trace', input_examples=[im]) # export
yaml_save(Path(f) / file.with_suffix('.yaml').name, metadata) # add metadata.yaml
return f, None
@try_export
def export_coreml(model, im, file, int8, half, prefix=colorstr('CoreML:')):
# YOLOv5 CoreML export
check_requirements('coremltools')
import coremltools as ct
LOGGER.info(f'\n{prefix} starting export with coremltools {ct.__version__}...')
f = file.with_suffix('.mlmodel')
ts = torch.jit.trace(model, im, strict=False) # TorchScript model
ct_model = ct.convert(ts, inputs=[ct.ImageType('image', shape=im.shape, scale=1 / 255, bias=[0, 0, 0])])
bits, mode = (8, 'kmeans_lut') if int8 else (16, 'linear') if half else (32, None)
if bits < 32:
if MACOS: # quantization only supported on macOS
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning) # suppress numpy==1.20 float warning
ct_model = ct.models.neural_network.quantization_utils.quantize_weights(ct_model, bits, mode)
else:
print(f'{prefix} quantization only supported on macOS, skipping...')
ct_model.save(f)
return f, ct_model
@try_export
def export_engine(model, im, file, half, dynamic, simplify, workspace=4, verbose=False, prefix=colorstr('TensorRT:')):
# YOLOv5 TensorRT export https://developer.nvidia.com/tensorrt
assert im.device.type != 'cpu', 'export running on CPU but must be on GPU, i.e. `python export.py --device 0`'
try:
import tensorrt as trt
except Exception:
if platform.system() == 'Linux':
check_requirements('nvidia-tensorrt', cmds='-U --index-url https://pypi.ngc.nvidia.com')
import tensorrt as trt
if trt.__version__[0] == '7': # TensorRT 7 handling https://github.com/ultralytics/yolov5/issues/6012
grid = model.model[-1].anchor_grid
model.model[-1].anchor_grid = [a[..., :1, :1, :] for a in grid]
export_onnx(model, im, file, 12, dynamic, simplify) # opset 12
model.model[-1].anchor_grid = grid
else: # TensorRT >= 8
check_version(trt.__version__, '8.0.0', hard=True) # require tensorrt>=8.0.0
export_onnx(model, im, file, 12, dynamic, simplify) # opset 12
onnx = file.with_suffix('.onnx')
LOGGER.info(f'\n{prefix} starting export with TensorRT {trt.__version__}...')
assert onnx.exists(), f'failed to export ONNX file: {onnx}'
f = file.with_suffix('.engine') # TensorRT engine file
logger = trt.Logger(trt.Logger.INFO)
if verbose:
logger.min_severity = trt.Logger.Severity.VERBOSE
builder = trt.Builder(logger)
config = builder.create_builder_config()
config.max_workspace_size = workspace * 1 << 30
# config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace << 30) # fix TRT 8.4 deprecation notice
flag = (1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
network = builder.create_network(flag)
parser = trt.OnnxParser(network, logger)
if not parser.parse_from_file(str(onnx)):
raise RuntimeError(f'failed to load ONNX file: {onnx}')
inputs = [network.get_input(i) for i in range(network.num_inputs)]
outputs = [network.get_output(i) for i in range(network.num_outputs)]
for inp in inputs:
LOGGER.info(f'{prefix} input "{inp.name}" with shape{inp.shape} {inp.dtype}')
for out in outputs:
LOGGER.info(f'{prefix} output "{out.name}" with shape{out.shape} {out.dtype}')
if dynamic:
if im.shape[0] <= 1:
LOGGER.warning(f'{prefix} WARNING ⚠️ --dynamic model requires maximum --batch-size argument')
profile = builder.create_optimization_profile()
for inp in inputs:
profile.set_shape(inp.name, (1, *im.shape[1:]), (max(1, im.shape[0] // 2), *im.shape[1:]), im.shape)
config.add_optimization_profile(profile)
LOGGER.info(f'{prefix} building FP{16 if builder.platform_has_fast_fp16 and half else 32} engine as {f}')
if builder.platform_has_fast_fp16 and half:
config.set_flag(trt.BuilderFlag.FP16)
with builder.build_engine(network, config) as engine, open(f, 'wb') as t:
t.write(engine.serialize())
return f, None
@try_export
def export_saved_model(model,
im,
file,
dynamic,
tf_nms=False,
agnostic_nms=False,
topk_per_class=100,
topk_all=100,
iou_thres=0.45,
conf_thres=0.25,
keras=False,
prefix=colorstr('TensorFlow SavedModel:')):
# YOLOv5 TensorFlow SavedModel export
try:
import tensorflow as tf
except Exception:
check_requirements(f"tensorflow{'' if torch.cuda.is_available() else '-macos' if MACOS else '-cpu'}")
import tensorflow as tf
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
from models.tf import TFModel
LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
f = str(file).replace('.pt', '_saved_model')
batch_size, ch, *imgsz = list(im.shape) # BCHW
tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz)
im = tf.zeros((batch_size, *imgsz, ch)) # BHWC order for TensorFlow
_ = tf_model.predict(im, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
inputs = tf.keras.Input(shape=(*imgsz, ch), batch_size=None if dynamic else batch_size)
outputs = tf_model.predict(inputs, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
keras_model = tf.keras.Model(inputs=inputs, outputs=outputs)
keras_model.trainable = False
keras_model.summary()
if keras:
keras_model.save(f, save_format='tf')
else:
spec = tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype)
m = tf.function(lambda x: keras_model(x)) # full model
m = m.get_concrete_function(spec)
frozen_func = convert_variables_to_constants_v2(m)
tfm = tf.Module()
tfm.__call__ = tf.function(lambda x: frozen_func(x)[:4] if tf_nms else frozen_func(x), [spec])
tfm.__call__(im)
tf.saved_model.save(tfm,
f,
options=tf.saved_model.SaveOptions(experimental_custom_gradients=False) if check_version(
tf.__version__, '2.6') else tf.saved_model.SaveOptions())
return f, keras_model
@try_export
def export_pb(keras_model, file, prefix=colorstr('TensorFlow GraphDef:')):
# YOLOv5 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow
import tensorflow as tf
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
f = file.with_suffix('.pb')
m = tf.function(lambda x: keras_model(x)) # full model
m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
frozen_func = convert_variables_to_constants_v2(m)
frozen_func.graph.as_graph_def()
tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)
return f, None
@try_export
def export_tflite(keras_model, im, file, int8, data, nms, agnostic_nms, prefix=colorstr('TensorFlow Lite:')):
# YOLOv5 TensorFlow Lite export
import tensorflow as tf
LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
batch_size, ch, *imgsz = list(im.shape) # BCHW
f = str(file).replace('.pt', '-fp16.tflite')
converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
converter.target_spec.supported_types = [tf.float16]
converter.optimizations = [tf.lite.Optimize.DEFAULT]
if int8:
from models.tf import representative_dataset_gen
dataset = LoadImages(check_dataset(check_yaml(data))['train'], img_size=imgsz, auto=False)
converter.representative_dataset = lambda: representative_dataset_gen(dataset, ncalib=100)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.target_spec.supported_types = []
converter.inference_input_type = tf.uint8 # or tf.int8
converter.inference_output_type = tf.uint8 # or tf.int8
converter.experimental_new_quantizer = True
f = str(file).replace('.pt', '-int8.tflite')
if nms or agnostic_nms:
converter.target_spec.supported_ops.append(tf.lite.OpsSet.SELECT_TF_OPS)
tflite_model = converter.convert()
open(f, 'wb').write(tflite_model)
return f, None
@try_export
def export_edgetpu(file, prefix=colorstr('Edge TPU:')):
# YOLOv5 Edge TPU export https://coral.ai/docs/edgetpu/models-intro/
cmd = 'edgetpu_compiler --version'
help_url = 'https://coral.ai/docs/edgetpu/compiler/'
assert platform.system() == 'Linux', f'export only supported on Linux. See {help_url}'
if subprocess.run(f'{cmd} > /dev/null 2>&1', shell=True).returncode != 0:
LOGGER.info(f'\n{prefix} export requires Edge TPU compiler. Attempting install from {help_url}')
sudo = subprocess.run('sudo --version >/dev/null', shell=True).returncode == 0 # sudo installed on system
for c in (
'curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -',
'echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | sudo tee /etc/apt/sources.list.d/coral-edgetpu.list',
'sudo apt-get update', 'sudo apt-get install edgetpu-compiler'):
subprocess.run(c if sudo else c.replace('sudo ', ''), shell=True, check=True)
ver = subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1]
LOGGER.info(f'\n{prefix} starting export with Edge TPU compiler {ver}...')
f = str(file).replace('.pt', '-int8_edgetpu.tflite') # Edge TPU model
f_tfl = str(file).replace('.pt', '-int8.tflite') # TFLite model
subprocess.run([
'edgetpu_compiler',
'-s',
'-d',
'-k',
'10',
'--out_dir',
str(file.parent),
f_tfl,], check=True)
return f, None
@try_export
def export_tfjs(file, int8, prefix=colorstr('TensorFlow.js:')):
# YOLOv5 TensorFlow.js export
check_requirements('tensorflowjs')
import tensorflowjs as tfjs
LOGGER.info(f'\n{prefix} starting export with tensorflowjs {tfjs.__version__}...')
f = str(file).replace('.pt', '_web_model') # js dir
f_pb = file.with_suffix('.pb') # *.pb path
f_json = f'{f}/model.json' # *.json path
args = [
'tensorflowjs_converter',
'--input_format=tf_frozen_model',
'--quantize_uint8' if int8 else '',
'--output_node_names=Identity,Identity_1,Identity_2,Identity_3',
str(f_pb),
str(f),]
subprocess.run([arg for arg in args if arg], check=True)
json = Path(f_json).read_text()
with open(f_json, 'w') as j: # sort JSON Identity_* in ascending order
subst = re.sub(
r'{"outputs": {"Identity.?.?": {"name": "Identity.?.?"}, '
r'"Identity.?.?": {"name": "Identity.?.?"}, '
r'"Identity.?.?": {"name": "Identity.?.?"}, '
r'"Identity.?.?": {"name": "Identity.?.?"}}}', r'{"outputs": {"Identity": {"name": "Identity"}, '
r'"Identity_1": {"name": "Identity_1"}, '
r'"Identity_2": {"name": "Identity_2"}, '
r'"Identity_3": {"name": "Identity_3"}}}', json)
j.write(subst)
return f, None
def add_tflite_metadata(file, metadata, num_outputs):
# Add metadata to *.tflite models per https://www.tensorflow.org/lite/models/convert/metadata
with contextlib.suppress(ImportError):
# check_requirements('tflite_support')
from tflite_support import flatbuffers
from tflite_support import metadata as _metadata
from tflite_support import metadata_schema_py_generated as _metadata_fb
tmp_file = Path('/tmp/meta.txt')
with open(tmp_file, 'w') as meta_f:
meta_f.write(str(metadata))
model_meta = _metadata_fb.ModelMetadataT()
label_file = _metadata_fb.AssociatedFileT()
label_file.name = tmp_file.name
model_meta.associatedFiles = [label_file]
subgraph = _metadata_fb.SubGraphMetadataT()
subgraph.inputTensorMetadata = [_metadata_fb.TensorMetadataT()]
subgraph.outputTensorMetadata = [_metadata_fb.TensorMetadataT()] * num_outputs
model_meta.subgraphMetadata = [subgraph]
b = flatbuffers.Builder(0)
b.Finish(model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER)
metadata_buf = b.Output()
populator = _metadata.MetadataPopulator.with_model_file(file)
populator.load_metadata_buffer(metadata_buf)
populator.load_associated_files([str(tmp_file)])
populator.populate()
tmp_file.unlink()
@smart_inference_mode()
def run(
data=ROOT / 'data/coco128.yaml', # 'dataset.yaml path'
weights=ROOT / 'yolov5s.pt', # weights path
imgsz=(640, 640), # image (height, width)
batch_size=1, # batch size
device='cpu', # cuda device, i.e. 0 or 0,1,2,3 or cpu
include=('torchscript', 'onnx'), # include formats
half=False, # FP16 half-precision export
inplace=False, # set YOLOv5 Detect() inplace=True
keras=False, # use Keras
optimize=False, # TorchScript: optimize for mobile
int8=False, # CoreML/TF INT8 quantization
dynamic=False, # ONNX/TF/TensorRT: dynamic axes
simplify=False, # ONNX: simplify model
opset=12, # ONNX: opset version
verbose=False, # TensorRT: verbose log
workspace=4, # TensorRT: workspace size (GB)
nms=False, # TF: add NMS to model
agnostic_nms=False, # TF: add agnostic NMS to model
topk_per_class=100, # TF.js NMS: topk per class to keep
topk_all=100, # TF.js NMS: topk for all classes to keep
iou_thres=0.45, # TF.js NMS: IoU threshold
conf_thres=0.25, # TF.js NMS: confidence threshold
):
t = time.time()
include = [x.lower() for x in include] # to lowercase
fmts = tuple(export_formats()['Argument'][1:]) # --include arguments
flags = [x in include for x in fmts]
assert sum(flags) == len(include), f'ERROR: Invalid --include {include}, valid --include arguments are {fmts}'
jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle = flags # export booleans
file = Path(url2file(weights) if str(weights).startswith(('http:/', 'https:/')) else weights) # PyTorch weights
# Load PyTorch model
device = select_device(device)
if half:
assert device.type != 'cpu' or coreml, '--half only compatible with GPU export, i.e. use --device 0'
assert not dynamic, '--half not compatible with --dynamic, i.e. use either --half or --dynamic but not both'
model = attempt_load(weights, device=device, inplace=True, fuse=True) # load FP32 model
# Checks
imgsz *= 2 if len(imgsz) == 1 else 1 # expand
if optimize:
assert device.type == 'cpu', '--optimize not compatible with cuda devices, i.e. use --device cpu'
# Input
gs = int(max(model.stride)) # grid size (max stride)
imgsz = [check_img_size(x, gs) for x in imgsz] # verify img_size are gs-multiples
im = torch.zeros(batch_size, 3, *imgsz).to(device) # image size(1,3,320,192) BCHW iDetection
# Update model
model.eval()
for k, m in model.named_modules():
if isinstance(m, Detect):
m.inplace = inplace
m.dynamic = dynamic
m.export = True
for _ in range(2):
y = model(im) # dry runs
if half and not coreml:
im, model = im.half(), model.half() # to FP16
shape = tuple((y[0] if isinstance(y, tuple) else y).shape) # model output shape
metadata = {'stride': int(max(model.stride)), 'names': model.names} # model metadata
LOGGER.info(f"\n{colorstr('PyTorch:')} starting from {file} with output shape {shape} ({file_size(file):.1f} MB)")
# Exports
f = [''] * len(fmts) # exported filenames
warnings.filterwarnings(action='ignore', category=torch.jit.TracerWarning) # suppress TracerWarning
if jit: # TorchScript
f[0], _ = export_torchscript(model, im, file, optimize)
if engine: # TensorRT required before ONNX
f[1], _ = export_engine(model, im, file, half, dynamic, simplify, workspace, verbose)
if onnx or xml: # OpenVINO requires ONNX
f[2], _ = export_onnx(model, im, file, opset, dynamic, simplify)
if xml: # OpenVINO
f[3], _ = export_openvino(file, metadata, half)
if coreml: # CoreML
f[4], _ = export_coreml(model, im, file, int8, half)
if any((saved_model, pb, tflite, edgetpu, tfjs)): # TensorFlow formats
assert not tflite or not tfjs, 'TFLite and TF.js models must be exported separately, please pass only one type.'
assert not isinstance(model, ClassificationModel), 'ClassificationModel export to TF formats not yet supported.'
f[5], s_model = export_saved_model(model.cpu(),
im,
file,
dynamic,
tf_nms=nms or agnostic_nms or tfjs,
agnostic_nms=agnostic_nms or tfjs,
topk_per_class=topk_per_class,
topk_all=topk_all,
iou_thres=iou_thres,
conf_thres=conf_thres,
keras=keras)
if pb or tfjs: # pb prerequisite to tfjs
f[6], _ = export_pb(s_model, file)
if tflite or edgetpu:
f[7], _ = export_tflite(s_model, im, file, int8 or edgetpu, data=data, nms=nms, agnostic_nms=agnostic_nms)
if edgetpu:
f[8], _ = export_edgetpu(file)
add_tflite_metadata(f[8] or f[7], metadata, num_outputs=len(s_model.outputs))
if tfjs:
f[9], _ = export_tfjs(file, int8)
if paddle: # PaddlePaddle
f[10], _ = export_paddle(model, im, file, metadata)
# Finish
f = [str(x) for x in f if x] # filter out '' and None
if any(f):
cls, det, seg = (isinstance(model, x) for x in (ClassificationModel, DetectionModel, SegmentationModel)) # type
det &= not seg # segmentation models inherit from SegmentationModel(DetectionModel)
dir = Path('segment' if seg else 'classify' if cls else '')
h = '--half' if half else '' # --half FP16 inference arg
s = '# WARNING ⚠️ ClassificationModel not yet supported for PyTorch Hub AutoShape inference' if cls else \
'# WARNING ⚠️ SegmentationModel not yet supported for PyTorch Hub AutoShape inference' if seg else ''
LOGGER.info(f'\nExport complete ({time.time() - t:.1f}s)'
f"\nResults saved to {colorstr('bold', file.parent.resolve())}"
f"\nDetect: python {dir / ('detect.py' if det else 'predict.py')} --weights {f[-1]} {h}"
f"\nValidate: python {dir / 'val.py'} --weights {f[-1]} {h}"
f"\nPyTorch Hub: model = torch.hub.load('ultralytics/yolov5', 'custom', '{f[-1]}') {s}"
f'\nVisualize: https://netron.app')
return f # return list of exported files/dirs
def parse_opt(known=False):
parser = argparse.ArgumentParser()
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640, 640], help='image (h, w)')
parser.add_argument('--batch-size', type=int, default=1, help='batch size')
parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
parser.add_argument('--keras', action='store_true', help='TF: use Keras')
parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
parser.add_argument('--int8', action='store_true', help='CoreML/TF INT8 quantization')
parser.add_argument('--dynamic', action='store_true', help='ONNX/TF/TensorRT: dynamic axes')
parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
parser.add_argument('--opset', type=int, default=17, help='ONNX: opset version')
parser.add_argument('--verbose', action='store_true', help='TensorRT: verbose log')
parser.add_argument('--workspace', type=int, default=4, help='TensorRT: workspace size (GB)')
parser.add_argument('--nms', action='store_true', help='TF: add NMS to model')
parser.add_argument('--agnostic-nms', action='store_true', help='TF: add agnostic NMS to model')
parser.add_argument('--topk-per-class', type=int, default=100, help='TF.js NMS: topk per class to keep')
parser.add_argument('--topk-all', type=int, default=100, help='TF.js NMS: topk for all classes to keep')
parser.add_argument('--iou-thres', type=float, default=0.45, help='TF.js NMS: IoU threshold')
parser.add_argument('--conf-thres', type=float, default=0.25, help='TF.js NMS: confidence threshold')
parser.add_argument(
'--include',
nargs='+',
default=['torchscript'],
help='torchscript, onnx, openvino, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle')
opt = parser.parse_known_args()[0] if known else parser.parse_args()
print_args(vars(opt))
return opt
def main(opt):
for opt.weights in (opt.weights if isinstance(opt.weights, list) else [opt.weights]):
run(**vars(opt))
if __name__ == '__main__':
opt = parse_opt()
main(opt)

View File

@ -25,7 +25,7 @@ def main():
aaRightShift = 0 aaRightShift = 0
# Autoaim mouse movement amplifier # Autoaim mouse movement amplifier
aaMovementAmp = 1.0 aaMovementAmp = .8
# Person Class Confidence # Person Class Confidence
confidence = 0.5 confidence = 0.5

View File

@ -94,7 +94,7 @@ def main():
sTime = time.time() sTime = time.time()
# Loading Yolo5 Small AI Model # Loading Yolo5 Small AI Model
model = DetectMultiBackend('yolov5s320Half.engine', device=torch.device( model = DetectMultiBackend('yolov5s.engine', device=torch.device(
'cuda'), dnn=False, data='', fp16=True) 'cuda'), dnn=False, data='', fp16=True)
stride, names, pt = model.stride, model.names, model.pt stride, names, pt = model.stride, model.names, model.pt

View File

@ -21,14 +21,13 @@ import pandas as pd
import requests import requests
import torch import torch
import torch.nn as nn import torch.nn as nn
from IPython.display import display
from PIL import Image from PIL import Image
from torch.cuda import amp from torch.cuda import amp
from utils import TryExcept from utils import TryExcept
from utils.dataloaders import exif_transpose, letterbox from utils.dataloaders import exif_transpose, letterbox
from utils.general import (LOGGER, ROOT, Profile, check_requirements, check_suffix, check_version, colorstr, from utils.general import (LOGGER, ROOT, Profile, check_requirements, check_suffix, check_version, colorstr,
increment_path, is_notebook, make_divisible, non_max_suppression, scale_boxes, xywh2xyxy, increment_path, is_jupyter, make_divisible, non_max_suppression, scale_boxes, xywh2xyxy,
xyxy2xywh, yaml_load) xyxy2xywh, yaml_load)
from utils.plots import Annotator, colors, save_one_box from utils.plots import Annotator, colors, save_one_box
from utils.torch_utils import copy_attr, smart_inference_mode from utils.torch_utils import copy_attr, smart_inference_mode
@ -37,8 +36,7 @@ from utils.torch_utils import copy_attr, smart_inference_mode
def autopad(k, p=None, d=1): # kernel, padding, dilation def autopad(k, p=None, d=1): # kernel, padding, dilation
# Pad to 'same' shape outputs # Pad to 'same' shape outputs
if d > 1: if d > 1:
k = d * (k - 1) + 1 if isinstance(k, k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
if p is None: if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
return p return p
@ -50,11 +48,9 @@ class Conv(nn.Module):
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
super().__init__() super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad( self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
k, p, d), groups=g, dilation=d, bias=False)
self.bn = nn.BatchNorm2d(c2) self.bn = nn.BatchNorm2d(c2)
self.act = self.default_act if act is True else act if isinstance( self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
act, nn.Module) else nn.Identity()
def forward(self, x): def forward(self, x):
return self.act(self.bn(self.conv(x))) return self.act(self.bn(self.conv(x)))
@ -65,15 +61,13 @@ class Conv(nn.Module):
class DWConv(Conv): class DWConv(Conv):
# Depth-wise convolution # Depth-wise convolution
# ch_in, ch_out, kernel, stride, dilation, activation def __init__(self, c1, c2, k=1, s=1, d=1, act=True): # ch_in, ch_out, kernel, stride, dilation, activation
def __init__(self, c1, c2, k=1, s=1, d=1, act=True):
super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act) super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)
class DWConvTranspose2d(nn.ConvTranspose2d): class DWConvTranspose2d(nn.ConvTranspose2d):
# Depth-wise transpose convolution # Depth-wise transpose convolution
# ch_in, ch_out, kernel, stride, padding, padding_out def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0): # ch_in, ch_out, kernel, stride, padding, padding_out
def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0):
super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2)) super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))
@ -102,8 +96,7 @@ class TransformerBlock(nn.Module):
if c1 != c2: if c1 != c2:
self.conv = Conv(c1, c2) self.conv = Conv(c1, c2)
self.linear = nn.Linear(c2, c2) # learnable position embedding self.linear = nn.Linear(c2, c2) # learnable position embedding
self.tr = nn.Sequential(*(TransformerLayer(c2, num_heads) self.tr = nn.Sequential(*(TransformerLayer(c2, num_heads) for _ in range(num_layers)))
for _ in range(num_layers)))
self.c2 = c2 self.c2 = c2
def forward(self, x): def forward(self, x):
@ -116,8 +109,7 @@ class TransformerBlock(nn.Module):
class Bottleneck(nn.Module): class Bottleneck(nn.Module):
# Standard bottleneck # Standard bottleneck
# ch_in, ch_out, shortcut, groups, expansion def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) self.cv1 = Conv(c1, c_, 1, 1)
@ -130,8 +122,7 @@ class Bottleneck(nn.Module):
class BottleneckCSP(nn.Module): class BottleneckCSP(nn.Module):
# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
# ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) self.cv1 = Conv(c1, c_, 1, 1)
@ -140,8 +131,7 @@ class BottleneckCSP(nn.Module):
self.cv4 = Conv(2 * c_, c2, 1, 1) self.cv4 = Conv(2 * c_, c2, 1, 1)
self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3) self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3)
self.act = nn.SiLU() self.act = nn.SiLU()
self.m = nn.Sequential( self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
def forward(self, x): def forward(self, x):
y1 = self.cv3(self.m(self.cv1(x))) y1 = self.cv3(self.m(self.cv1(x)))
@ -165,15 +155,13 @@ class CrossConv(nn.Module):
class C3(nn.Module): class C3(nn.Module):
# CSP Bottleneck with 3 convolutions # CSP Bottleneck with 3 convolutions
# ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2) self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
self.m = nn.Sequential( self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
def forward(self, x): def forward(self, x):
return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1)) return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
@ -184,8 +172,7 @@ class C3x(C3):
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e) super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e) c_ = int(c2 * e)
self.m = nn.Sequential( self.m = nn.Sequential(*(CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)))
*(CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)))
class C3TR(C3): class C3TR(C3):
@ -219,14 +206,12 @@ class SPP(nn.Module):
c_ = c1 // 2 # hidden channels c_ = c1 // 2 # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
self.m = nn.ModuleList( self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])
[nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])
def forward(self, x): def forward(self, x):
x = self.cv1(x) x = self.cv1(x)
with warnings.catch_warnings(): with warnings.catch_warnings():
# suppress torch 1.9.0 max_pool2d() warning warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning
warnings.simplefilter('ignore')
return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
@ -242,8 +227,7 @@ class SPPF(nn.Module):
def forward(self, x): def forward(self, x):
x = self.cv1(x) x = self.cv1(x)
with warnings.catch_warnings(): with warnings.catch_warnings():
# suppress torch 1.9.0 max_pool2d() warning warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning
warnings.simplefilter('ignore')
y1 = self.m(x) y1 = self.m(x)
y2 = self.m(y1) y2 = self.m(y1)
return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1)) return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))
@ -251,8 +235,7 @@ class SPPF(nn.Module):
class Focus(nn.Module): class Focus(nn.Module):
# Focus wh information into c-space # Focus wh information into c-space
# ch_in, ch_out, kernel, stride, padding, groups def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):
super().__init__() super().__init__()
self.conv = Conv(c1 * 4, c2, k, s, p, g, act=act) self.conv = Conv(c1 * 4, c2, k, s, p, g, act=act)
# self.contract = Contract(gain=2) # self.contract = Contract(gain=2)
@ -264,8 +247,7 @@ class Focus(nn.Module):
class GhostConv(nn.Module): class GhostConv(nn.Module):
# Ghost Convolution https://github.com/huawei-noah/ghostnet # Ghost Convolution https://github.com/huawei-noah/ghostnet
# ch_in, ch_out, kernel, stride, groups def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups
def __init__(self, c1, c2, k=1, s=1, g=1, act=True):
super().__init__() super().__init__()
c_ = c2 // 2 # hidden channels c_ = c2 // 2 # hidden channels
self.cv1 = Conv(c1, c_, k, s, None, g, act=act) self.cv1 = Conv(c1, c_, k, s, None, g, act=act)
@ -346,34 +328,28 @@ class DetectMultiBackend(nn.Module):
# TensorFlow Lite: *.tflite # TensorFlow Lite: *.tflite
# TensorFlow Edge TPU: *_edgetpu.tflite # TensorFlow Edge TPU: *_edgetpu.tflite
# PaddlePaddle: *_paddle_model # PaddlePaddle: *_paddle_model
# scoped to avoid circular import from models.experimental import attempt_download, attempt_load # scoped to avoid circular import
from models.experimental import attempt_download, attempt_load
super().__init__() super().__init__()
w = str(weights[0] if isinstance(weights, list) else weights) w = str(weights[0] if isinstance(weights, list) else weights)
pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle, triton = self._model_type( pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle, triton = self._model_type(w)
w)
fp16 &= pt or jit or onnx or engine # FP16 fp16 &= pt or jit or onnx or engine # FP16
# BHWC formats (vs torch BCWH) nhwc = coreml or saved_model or pb or tflite or edgetpu # BHWC formats (vs torch BCWH)
nhwc = coreml or saved_model or pb or tflite or edgetpu
stride = 32 # default stride stride = 32 # default stride
cuda = torch.cuda.is_available() and device.type != 'cpu' # use CUDA cuda = torch.cuda.is_available() and device.type != 'cpu' # use CUDA
if not (pt or triton): if not (pt or triton):
w = attempt_download(w) # download if not local w = attempt_download(w) # download if not local
if pt: # PyTorch if pt: # PyTorch
model = attempt_load(weights if isinstance( model = attempt_load(weights if isinstance(weights, list) else w, device=device, inplace=True, fuse=fuse)
weights, list) else w, device=device, inplace=True, fuse=fuse)
stride = max(int(model.stride.max()), 32) # model stride stride = max(int(model.stride.max()), 32) # model stride
names = model.module.names if hasattr( names = model.module.names if hasattr(model, 'module') else model.names # get class names
model, 'module') else model.names # get class names
model.half() if fp16 else model.float() model.half() if fp16 else model.float()
self.model = model # explicitly assign for to(), cpu(), cuda(), half() self.model = model # explicitly assign for to(), cpu(), cuda(), half()
elif jit: # TorchScript elif jit: # TorchScript
LOGGER.info(f'Loading {w} for TorchScript inference...') LOGGER.info(f'Loading {w} for TorchScript inference...')
extra_files = {'config.txt': ''} # model metadata extra_files = {'config.txt': ''} # model metadata
model = torch.jit.load( model = torch.jit.load(w, _extra_files=extra_files, map_location=device)
w, _extra_files=extra_files, map_location=device)
model.half() if fp16 else model.float() model.half() if fp16 else model.float()
if extra_files['config.txt']: # load metadata dict if extra_files['config.txt']: # load metadata dict
d = json.loads(extra_files['config.txt'], d = json.loads(extra_files['config.txt'],
@ -386,11 +362,9 @@ class DetectMultiBackend(nn.Module):
net = cv2.dnn.readNetFromONNX(w) net = cv2.dnn.readNetFromONNX(w)
elif onnx: # ONNX Runtime elif onnx: # ONNX Runtime
LOGGER.info(f'Loading {w} for ONNX Runtime inference...') LOGGER.info(f'Loading {w} for ONNX Runtime inference...')
check_requirements( check_requirements(('onnx', 'onnxruntime-gpu' if cuda else 'onnxruntime'))
('onnx', 'onnxruntime-gpu' if cuda else 'onnxruntime'))
import onnxruntime import onnxruntime
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if cuda else [ providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if cuda else ['CPUExecutionProvider']
'CPUExecutionProvider']
session = onnxruntime.InferenceSession(w, providers=providers) session = onnxruntime.InferenceSession(w, providers=providers)
output_names = [x.name for x in session.get_outputs()] output_names = [x.name for x in session.get_outputs()]
meta = session.get_modelmeta().custom_metadata_map # metadata meta = session.get_modelmeta().custom_metadata_map # metadata
@ -398,33 +372,26 @@ class DetectMultiBackend(nn.Module):
stride, names = int(meta['stride']), eval(meta['names']) stride, names = int(meta['stride']), eval(meta['names'])
elif xml: # OpenVINO elif xml: # OpenVINO
LOGGER.info(f'Loading {w} for OpenVINO inference...') LOGGER.info(f'Loading {w} for OpenVINO inference...')
# requires openvino-dev: https://pypi.org/project/openvino-dev/ check_requirements('openvino') # requires openvino-dev: https://pypi.org/project/openvino-dev/
check_requirements('openvino')
from openvino.runtime import Core, Layout, get_batch from openvino.runtime import Core, Layout, get_batch
ie = Core() ie = Core()
if not Path(w).is_file(): # if not *.xml if not Path(w).is_file(): # if not *.xml
# get *.xml file from *_openvino_model dir w = next(Path(w).glob('*.xml')) # get *.xml file from *_openvino_model dir
w = next(Path(w).glob('*.xml')) network = ie.read_model(model=w, weights=Path(w).with_suffix('.bin'))
network = ie.read_model(
model=w, weights=Path(w).with_suffix('.bin'))
if network.get_parameters()[0].get_layout().empty: if network.get_parameters()[0].get_layout().empty:
network.get_parameters()[0].set_layout(Layout("NCHW")) network.get_parameters()[0].set_layout(Layout('NCHW'))
batch_dim = get_batch(network) batch_dim = get_batch(network)
if batch_dim.is_static: if batch_dim.is_static:
batch_size = batch_dim.get_length() batch_size = batch_dim.get_length()
# device_name="MYRIAD" for Intel NCS2 executable_network = ie.compile_model(network, device_name='CPU') # device_name="MYRIAD" for Intel NCS2
executable_network = ie.compile_model(network, device_name="CPU") stride, names = self._load_metadata(Path(w).with_suffix('.yaml')) # load metadata
stride, names = self._load_metadata(
Path(w).with_suffix('.yaml')) # load metadata
elif engine: # TensorRT elif engine: # TensorRT
LOGGER.info(f'Loading {w} for TensorRT inference...') LOGGER.info(f'Loading {w} for TensorRT inference...')
import tensorrt as trt # https://developer.nvidia.com/nvidia-tensorrt-download import tensorrt as trt # https://developer.nvidia.com/nvidia-tensorrt-download
# require tensorrt>=7.0.0 check_version(trt.__version__, '7.0.0', hard=True) # require tensorrt>=7.0.0
check_version(trt.__version__, '7.0.0', hard=True)
if device.type == 'cpu': if device.type == 'cpu':
device = torch.device('cuda:0') device = torch.device('cuda:0')
Binding = namedtuple( Binding = namedtuple('Binding', ('name', 'dtype', 'shape', 'data', 'ptr'))
'Binding', ('name', 'dtype', 'shape', 'data', 'ptr'))
logger = trt.Logger(trt.Logger.INFO) logger = trt.Logger(trt.Logger.INFO)
with open(w, 'rb') as f, trt.Runtime(logger) as runtime: with open(w, 'rb') as f, trt.Runtime(logger) as runtime:
model = runtime.deserialize_cuda_engine(f.read()) model = runtime.deserialize_cuda_engine(f.read())
@ -439,20 +406,16 @@ class DetectMultiBackend(nn.Module):
if model.binding_is_input(i): if model.binding_is_input(i):
if -1 in tuple(model.get_binding_shape(i)): # dynamic if -1 in tuple(model.get_binding_shape(i)): # dynamic
dynamic = True dynamic = True
context.set_binding_shape( context.set_binding_shape(i, tuple(model.get_profile_shape(0, i)[2]))
i, tuple(model.get_profile_shape(0, i)[2]))
if dtype == np.float16: if dtype == np.float16:
fp16 = True fp16 = True
else: # output else: # output
output_names.append(name) output_names.append(name)
shape = tuple(context.get_binding_shape(i)) shape = tuple(context.get_binding_shape(i))
im = torch.from_numpy(np.empty(shape, dtype=dtype)).to(device) im = torch.from_numpy(np.empty(shape, dtype=dtype)).to(device)
bindings[name] = Binding( bindings[name] = Binding(name, dtype, shape, im, int(im.data_ptr()))
name, dtype, shape, im, int(im.data_ptr())) binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items())
binding_addrs = OrderedDict((n, d.ptr) batch_size = bindings['images'].shape[0] # if dynamic, this is instead max batch size
for n, d in bindings.items())
# if dynamic, this is instead max batch size
batch_size = bindings['images'].shape[0]
elif coreml: # CoreML elif coreml: # CoreML
LOGGER.info(f'Loading {w} for CoreML inference...') LOGGER.info(f'Loading {w} for CoreML inference...')
import coremltools as ct import coremltools as ct
@ -461,15 +424,13 @@ class DetectMultiBackend(nn.Module):
LOGGER.info(f'Loading {w} for TensorFlow SavedModel inference...') LOGGER.info(f'Loading {w} for TensorFlow SavedModel inference...')
import tensorflow as tf import tensorflow as tf
keras = False # assume TF1 saved_model keras = False # assume TF1 saved_model
model = tf.keras.models.load_model( model = tf.keras.models.load_model(w) if keras else tf.saved_model.load(w)
w) if keras else tf.saved_model.load(w)
elif pb: # GraphDef https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt elif pb: # GraphDef https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt
LOGGER.info(f'Loading {w} for TensorFlow GraphDef inference...') LOGGER.info(f'Loading {w} for TensorFlow GraphDef inference...')
import tensorflow as tf import tensorflow as tf
def wrap_frozen_graph(gd, inputs, outputs): def wrap_frozen_graph(gd, inputs, outputs):
x = tf.compat.v1.wrap_function( x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=''), []) # wrapped
lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped
ge = x.graph.as_graph_element ge = x.graph.as_graph_element
return x.prune(tf.nest.map_structure(ge, inputs), tf.nest.map_structure(ge, outputs)) return x.prune(tf.nest.map_structure(ge, inputs), tf.nest.map_structure(ge, outputs))
@ -483,8 +444,7 @@ class DetectMultiBackend(nn.Module):
gd = tf.Graph().as_graph_def() # TF GraphDef gd = tf.Graph().as_graph_def() # TF GraphDef
with open(w, 'rb') as f: with open(w, 'rb') as f:
gd.ParseFromString(f.read()) gd.ParseFromString(f.read())
frozen_func = wrap_frozen_graph( frozen_func = wrap_frozen_graph(gd, inputs='x:0', outputs=gd_outputs(gd))
gd, inputs="x:0", outputs=gd_outputs(gd))
elif tflite or edgetpu: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python elif tflite or edgetpu: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python
try: # https://coral.ai/docs/edgetpu/tflite-python/#update-existing-tf-lite-code-for-the-edge-tpu try: # https://coral.ai/docs/edgetpu/tflite-python/#update-existing-tf-lite-code-for-the-edge-tpu
from tflite_runtime.interpreter import Interpreter, load_delegate from tflite_runtime.interpreter import Interpreter, load_delegate
@ -492,14 +452,12 @@ class DetectMultiBackend(nn.Module):
import tensorflow as tf import tensorflow as tf
Interpreter, load_delegate = tf.lite.Interpreter, tf.lite.experimental.load_delegate, Interpreter, load_delegate = tf.lite.Interpreter, tf.lite.experimental.load_delegate,
if edgetpu: # TF Edge TPU https://coral.ai/software/#edgetpu-runtime if edgetpu: # TF Edge TPU https://coral.ai/software/#edgetpu-runtime
LOGGER.info( LOGGER.info(f'Loading {w} for TensorFlow Lite Edge TPU inference...')
f'Loading {w} for TensorFlow Lite Edge TPU inference...')
delegate = { delegate = {
'Linux': 'libedgetpu.so.1', 'Linux': 'libedgetpu.so.1',
'Darwin': 'libedgetpu.1.dylib', 'Darwin': 'libedgetpu.1.dylib',
'Windows': 'edgetpu.dll'}[platform.system()] 'Windows': 'edgetpu.dll'}[platform.system()]
interpreter = Interpreter(model_path=w, experimental_delegates=[ interpreter = Interpreter(model_path=w, experimental_delegates=[load_delegate(delegate)])
load_delegate(delegate)])
else: # TFLite else: # TFLite
LOGGER.info(f'Loading {w} for TensorFlow Lite inference...') LOGGER.info(f'Loading {w} for TensorFlow Lite inference...')
interpreter = Interpreter(model_path=w) # load TFLite model interpreter = Interpreter(model_path=w) # load TFLite model
@ -508,46 +466,39 @@ class DetectMultiBackend(nn.Module):
output_details = interpreter.get_output_details() # outputs output_details = interpreter.get_output_details() # outputs
# load metadata # load metadata
with contextlib.suppress(zipfile.BadZipFile): with contextlib.suppress(zipfile.BadZipFile):
with zipfile.ZipFile(w, "r") as model: with zipfile.ZipFile(w, 'r') as model:
meta_file = model.namelist()[0] meta_file = model.namelist()[0]
meta = ast.literal_eval( meta = ast.literal_eval(model.read(meta_file).decode('utf-8'))
model.read(meta_file).decode("utf-8"))
stride, names = int(meta['stride']), meta['names'] stride, names = int(meta['stride']), meta['names']
elif tfjs: # TF.js elif tfjs: # TF.js
raise NotImplementedError( raise NotImplementedError('ERROR: YOLOv5 TF.js inference is not supported')
'ERROR: YOLOv5 TF.js inference is not supported')
elif paddle: # PaddlePaddle elif paddle: # PaddlePaddle
LOGGER.info(f'Loading {w} for PaddlePaddle inference...') LOGGER.info(f'Loading {w} for PaddlePaddle inference...')
check_requirements('paddlepaddle-gpu' if cuda else 'paddlepaddle') check_requirements('paddlepaddle-gpu' if cuda else 'paddlepaddle')
import paddle.inference as pdi import paddle.inference as pdi
if not Path(w).is_file(): # if not *.pdmodel if not Path(w).is_file(): # if not *.pdmodel
# get *.pdmodel file from *_paddle_model dir w = next(Path(w).rglob('*.pdmodel')) # get *.pdmodel file from *_paddle_model dir
w = next(Path(w).rglob('*.pdmodel'))
weights = Path(w).with_suffix('.pdiparams') weights = Path(w).with_suffix('.pdiparams')
config = pdi.Config(str(w), str(weights)) config = pdi.Config(str(w), str(weights))
if cuda: if cuda:
config.enable_use_gpu( config.enable_use_gpu(memory_pool_init_size_mb=2048, device_id=0)
memory_pool_init_size_mb=2048, device_id=0)
predictor = pdi.create_predictor(config) predictor = pdi.create_predictor(config)
input_handle = predictor.get_input_handle( input_handle = predictor.get_input_handle(predictor.get_input_names()[0])
predictor.get_input_names()[0])
output_names = predictor.get_output_names() output_names = predictor.get_output_names()
elif triton: # NVIDIA Triton Inference Server elif triton: # NVIDIA Triton Inference Server
LOGGER.info(f'Using {w} as Triton Inference Server...') LOGGER.info(f'Using {w} as Triton Inference Server...')
check_requirements('tritonclient[all]') check_requirements('tritonclient[all]')
from utils.triton import TritonRemoteModel from utils.triton import TritonRemoteModel
model = TritonRemoteModel(url=w) model = TritonRemoteModel(url=w)
nhwc = model.runtime.startswith("tensorflow") nhwc = model.runtime.startswith('tensorflow')
else: else:
raise NotImplementedError(f'ERROR: {w} is not a supported format') raise NotImplementedError(f'ERROR: {w} is not a supported format')
# class names # class names
if 'names' not in locals(): if 'names' not in locals():
names = yaml_load(data)['names'] if data else { names = yaml_load(data)['names'] if data else {i: f'class{i}' for i in range(999)}
i: f'class{i}' for i in range(999)}
if names[0] == 'n01440764' and len(names) == 1000: # ImageNet if names[0] == 'n01440764' and len(names) == 1000: # ImageNet
# human-readable names names = yaml_load(ROOT / 'data/ImageNet.yaml')['names'] # human-readable names
names = yaml_load(ROOT / 'data/ImageNet.yaml')['names']
self.__dict__.update(locals()) # assign all variables to self self.__dict__.update(locals()) # assign all variables to self
@ -557,12 +508,10 @@ class DetectMultiBackend(nn.Module):
if self.fp16 and im.dtype != torch.float16: if self.fp16 and im.dtype != torch.float16:
im = im.half() # to FP16 im = im.half() # to FP16
if self.nhwc: if self.nhwc:
# torch BCHW to numpy BHWC shape(1,320,192,3) im = im.permute(0, 2, 3, 1) # torch BCHW to numpy BHWC shape(1,320,192,3)
im = im.permute(0, 2, 3, 1)
if self.pt: # PyTorch if self.pt: # PyTorch
y = self.model( y = self.model(im, augment=augment, visualize=visualize) if augment or visualize else self.model(im)
im, augment=augment, visualize=visualize) if augment or visualize else self.model(im)
elif self.jit: # TorchScript elif self.jit: # TorchScript
y = self.model(im) y = self.model(im)
elif self.dnn: # ONNX OpenCV DNN elif self.dnn: # ONNX OpenCV DNN
@ -571,22 +520,18 @@ class DetectMultiBackend(nn.Module):
y = self.net.forward() y = self.net.forward()
elif self.onnx: # ONNX Runtime elif self.onnx: # ONNX Runtime
im = im.cpu().numpy() # torch to numpy im = im.cpu().numpy() # torch to numpy
y = self.session.run(self.output_names, { y = self.session.run(self.output_names, {self.session.get_inputs()[0].name: im})
self.session.get_inputs()[0].name: im})
elif self.xml: # OpenVINO elif self.xml: # OpenVINO
im = im.cpu().numpy() # FP32 im = im.cpu().numpy() # FP32
y = list(self.executable_network([im]).values()) y = list(self.executable_network([im]).values())
elif self.engine: # TensorRT elif self.engine: # TensorRT
if self.dynamic and im.shape != self.bindings['images'].shape: if self.dynamic and im.shape != self.bindings['images'].shape:
i = self.model.get_binding_index('images') i = self.model.get_binding_index('images')
self.context.set_binding_shape( self.context.set_binding_shape(i, im.shape) # reshape if dynamic
i, im.shape) # reshape if dynamic self.bindings['images'] = self.bindings['images']._replace(shape=im.shape)
self.bindings['images'] = self.bindings['images']._replace(
shape=im.shape)
for name in self.output_names: for name in self.output_names:
i = self.model.get_binding_index(name) i = self.model.get_binding_index(name)
self.bindings[name].data.resize_( self.bindings[name].data.resize_(tuple(self.context.get_binding_shape(i)))
tuple(self.context.get_binding_shape(i)))
s = self.bindings['images'].shape s = self.bindings['images'].shape
assert im.shape == s, f"input size {im.shape} {'>' if self.dynamic else 'not equal to'} max model size {s}" assert im.shape == s, f"input size {im.shape} {'>' if self.dynamic else 'not equal to'} max model size {s}"
self.binding_addrs['images'] = int(im.data_ptr()) self.binding_addrs['images'] = int(im.data_ptr())
@ -596,37 +541,29 @@ class DetectMultiBackend(nn.Module):
im = im.cpu().numpy() im = im.cpu().numpy()
im = Image.fromarray((im[0] * 255).astype('uint8')) im = Image.fromarray((im[0] * 255).astype('uint8'))
# im = im.resize((192, 320), Image.ANTIALIAS) # im = im.resize((192, 320), Image.ANTIALIAS)
# coordinates are xywh normalized y = self.model.predict({'image': im}) # coordinates are xywh normalized
y = self.model.predict({'image': im})
if 'confidence' in y: if 'confidence' in y:
box = xywh2xyxy(y['coordinates'] * box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]]) # xyxy pixels
[[w, h, w, h]]) # xyxy pixels conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float)
conf, cls = y['confidence'].max( y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)
1), y['confidence'].argmax(1).astype(np.float)
y = np.concatenate(
(box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)
else: else:
# reversed for segmentation models (pred, proto) y = list(reversed(y.values())) # reversed for segmentation models (pred, proto)
y = list(reversed(y.values()))
elif self.paddle: # PaddlePaddle elif self.paddle: # PaddlePaddle
im = im.cpu().numpy().astype(np.float32) im = im.cpu().numpy().astype(np.float32)
self.input_handle.copy_from_cpu(im) self.input_handle.copy_from_cpu(im)
self.predictor.run() self.predictor.run()
y = [self.predictor.get_output_handle( y = [self.predictor.get_output_handle(x).copy_to_cpu() for x in self.output_names]
x).copy_to_cpu() for x in self.output_names]
elif self.triton: # NVIDIA Triton Inference Server elif self.triton: # NVIDIA Triton Inference Server
y = self.model(im) y = self.model(im)
else: # TensorFlow (SavedModel, GraphDef, Lite, Edge TPU) else: # TensorFlow (SavedModel, GraphDef, Lite, Edge TPU)
im = im.cpu().numpy() im = im.cpu().numpy()
if self.saved_model: # SavedModel if self.saved_model: # SavedModel
y = self.model( y = self.model(im, training=False) if self.keras else self.model(im)
im, training=False) if self.keras else self.model(im)
elif self.pb: # GraphDef elif self.pb: # GraphDef
y = self.frozen_func(x=self.tf.constant(im)) y = self.frozen_func(x=self.tf.constant(im))
else: # Lite or Edge TPU else: # Lite or Edge TPU
input = self.input_details[0] input = self.input_details[0]
# is TFLite quantized uint8 model int8 = input['dtype'] == np.uint8 # is TFLite quantized uint8 model
int8 = input['dtype'] == np.uint8
if int8: if int8:
scale, zero_point = input['quantization'] scale, zero_point = input['quantization']
im = (im / scale + zero_point).astype(np.uint8) # de-scale im = (im / scale + zero_point).astype(np.uint8) # de-scale
@ -637,8 +574,7 @@ class DetectMultiBackend(nn.Module):
x = self.interpreter.get_tensor(output['index']) x = self.interpreter.get_tensor(output['index'])
if int8: if int8:
scale, zero_point = output['quantization'] scale, zero_point = output['quantization']
x = (x.astype(np.float32) - zero_point) * \ x = (x.astype(np.float32) - zero_point) * scale # re-scale
scale # re-scale
y.append(x) y.append(x)
y = [x if isinstance(x, np.ndarray) else x.numpy() for x in y] y = [x if isinstance(x, np.ndarray) else x.numpy() for x in y]
y[0][..., :4] *= [w, h, w, h] # xywh normalized to pixels y[0][..., :4] *= [w, h, w, h] # xywh normalized to pixels
@ -655,8 +591,7 @@ class DetectMultiBackend(nn.Module):
# Warmup model by running inference once # Warmup model by running inference once
warmup_types = self.pt, self.jit, self.onnx, self.engine, self.saved_model, self.pb, self.triton warmup_types = self.pt, self.jit, self.onnx, self.engine, self.saved_model, self.pb, self.triton
if any(warmup_types) and (self.device.type != 'cpu' or self.triton): if any(warmup_types) and (self.device.type != 'cpu' or self.triton):
im = torch.empty( im = torch.empty(*imgsz, dtype=torch.half if self.fp16 else torch.float, device=self.device) # input
*imgsz, dtype=torch.half if self.fp16 else torch.float, device=self.device) # input
for _ in range(2 if self.jit else 1): # for _ in range(2 if self.jit else 1): #
self.forward(im) # warmup self.forward(im) # warmup
@ -664,16 +599,15 @@ class DetectMultiBackend(nn.Module):
def _model_type(p='path/to/model.pt'): def _model_type(p='path/to/model.pt'):
# Return model type from model path, i.e. path='path/to/model.onnx' -> type=onnx # Return model type from model path, i.e. path='path/to/model.onnx' -> type=onnx
# types = [pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle] # types = [pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle]
from export import export_formats
from utils.downloads import is_url from utils.downloads import is_url
sf = ['.pt', '.torchscript', '.onnx', '_openvino_model', '.engine', '.mlmodel', '_saved_model', sf = list(export_formats().Suffix) # export suffixes
'.pb', '.tflite', '_edgetpu.tflite', '_web_model', '_paddle_model'] # export suffixes
if not is_url(p, check=False): if not is_url(p, check=False):
check_suffix(p, sf) # checks check_suffix(p, sf) # checks
url = urlparse(p) # if url may be Triton inference server url = urlparse(p) # if url may be Triton inference server
types = [s in Path(p).name for s in sf] types = [s in Path(p).name for s in sf]
types[8] &= not types[9] # tflite &= not edgetpu types[8] &= not types[9] # tflite &= not edgetpu
triton = not any(types) and all( triton = not any(types) and all([any(s in url.scheme for s in ['http', 'grpc']), url.netloc])
[any(s in url.scheme for s in ["http", "grpc"]), url.netloc])
return types + [triton] return types + [triton]
@staticmethod @staticmethod
@ -691,8 +625,7 @@ class AutoShape(nn.Module):
iou = 0.45 # NMS IoU threshold iou = 0.45 # NMS IoU threshold
agnostic = False # NMS class-agnostic agnostic = False # NMS class-agnostic
multi_label = False # NMS multiple labels per box multi_label = False # NMS multiple labels per box
# (optional list) filter by class, i.e. = [0, 15, 16] for COCO persons, cats and dogs classes = None # (optional list) filter by class, i.e. = [0, 15, 16] for COCO persons, cats and dogs
classes = None
max_det = 1000 # maximum number of detections per image max_det = 1000 # maximum number of detections per image
amp = False # Automatic Mixed Precision (AMP) inference amp = False # Automatic Mixed Precision (AMP) inference
@ -700,15 +633,12 @@ class AutoShape(nn.Module):
super().__init__() super().__init__()
if verbose: if verbose:
LOGGER.info('Adding AutoShape... ') LOGGER.info('Adding AutoShape... ')
copy_attr(self, model, include=('yaml', 'nc', 'hyp', 'names', copy_attr(self, model, include=('yaml', 'nc', 'hyp', 'names', 'stride', 'abc'), exclude=()) # copy attributes
'stride', 'abc'), exclude=()) # copy attributes self.dmb = isinstance(model, DetectMultiBackend) # DetectMultiBackend() instance
# DetectMultiBackend() instance
self.dmb = isinstance(model, DetectMultiBackend)
self.pt = not self.dmb or model.pt # PyTorch model self.pt = not self.dmb or model.pt # PyTorch model
self.model = model.eval() self.model = model.eval()
if self.pt: if self.pt:
# Detect() m = self.model.model.model[-1] if self.dmb else self.model.model[-1] # Detect()
m = self.model.model.model[-1] if self.dmb else self.model.model[-1]
m.inplace = False # Detect.inplace=False for safe multithread inference m.inplace = False # Detect.inplace=False for safe multithread inference
m.export = True # do not output loss values m.export = True # do not output loss values
@ -716,8 +646,7 @@ class AutoShape(nn.Module):
# Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers # Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers
self = super()._apply(fn) self = super()._apply(fn)
if self.pt: if self.pt:
# Detect() m = self.model.model.model[-1] if self.dmb else self.model.model[-1] # Detect()
m = self.model.model.model[-1] if self.dmb else self.model.model[-1]
m.stride = fn(m.stride) m.stride = fn(m.stride)
m.grid = list(map(fn, m.grid)) m.grid = list(map(fn, m.grid))
if isinstance(m.anchor_grid, list): if isinstance(m.anchor_grid, list):
@ -739,47 +668,35 @@ class AutoShape(nn.Module):
with dt[0]: with dt[0]:
if isinstance(size, int): # expand if isinstance(size, int): # expand
size = (size, size) size = (size, size)
p = next(self.model.parameters()) if self.pt else torch.empty( p = next(self.model.parameters()) if self.pt else torch.empty(1, device=self.model.device) # param
1, device=self.model.device) # param autocast = self.amp and (p.device.type != 'cpu') # Automatic Mixed Precision (AMP) inference
# Automatic Mixed Precision (AMP) inference
autocast = self.amp and (p.device.type != 'cpu')
if isinstance(ims, torch.Tensor): # torch if isinstance(ims, torch.Tensor): # torch
with amp.autocast(autocast): with amp.autocast(autocast):
# inference return self.model(ims.to(p.device).type_as(p), augment=augment) # inference
return self.model(ims.to(p.device).type_as(p), augment=augment)
# Pre-process # Pre-process
n, ims = (len(ims), list(ims)) if isinstance( n, ims = (len(ims), list(ims)) if isinstance(ims, (list, tuple)) else (1, [ims]) # number, list of images
ims, (list, tuple)) else (1, [ims]) # number, list of images
shape0, shape1, files = [], [], [] # image and inference shapes, filenames shape0, shape1, files = [], [], [] # image and inference shapes, filenames
for i, im in enumerate(ims): for i, im in enumerate(ims):
f = f'image{i}' # filename f = f'image{i}' # filename
if isinstance(im, (str, Path)): # filename or uri if isinstance(im, (str, Path)): # filename or uri
im, f = Image.open(requests.get(im, stream=True).raw if str( im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith('http') else im), im
im).startswith('http') else im), im
im = np.asarray(exif_transpose(im)) im = np.asarray(exif_transpose(im))
elif isinstance(im, Image.Image): # PIL Image elif isinstance(im, Image.Image): # PIL Image
im, f = np.asarray(exif_transpose(im)), getattr( im, f = np.asarray(exif_transpose(im)), getattr(im, 'filename', f) or f
im, 'filename', f) or f
files.append(Path(f).with_suffix('.jpg').name) files.append(Path(f).with_suffix('.jpg').name)
if im.shape[0] < 5: # image in CHW if im.shape[0] < 5: # image in CHW
# reverse dataloader .transpose(2, 0, 1) im = im.transpose((1, 2, 0)) # reverse dataloader .transpose(2, 0, 1)
im = im.transpose((1, 2, 0)) im = im[..., :3] if im.ndim == 3 else cv2.cvtColor(im, cv2.COLOR_GRAY2BGR) # enforce 3ch input
im = im[..., :3] if im.ndim == 3 else cv2.cvtColor(
im, cv2.COLOR_GRAY2BGR) # enforce 3ch input
s = im.shape[:2] # HWC s = im.shape[:2] # HWC
shape0.append(s) # image shape shape0.append(s) # image shape
g = max(size) / max(s) # gain g = max(size) / max(s) # gain
shape1.append([int(y * g) for y in s]) shape1.append([int(y * g) for y in s])
ims[i] = im if im.data.contiguous else np.ascontiguousarray( ims[i] = im if im.data.contiguous else np.ascontiguousarray(im) # update
im) # update shape1 = [make_divisible(x, self.stride) for x in np.array(shape1).max(0)] # inf shape
shape1 = [make_divisible(x, self.stride)
for x in np.array(shape1).max(0)] # inf shape
x = [letterbox(im, shape1, auto=False)[0] for im in ims] # pad x = [letterbox(im, shape1, auto=False)[0] for im in ims] # pad
x = np.ascontiguousarray(np.array(x).transpose( x = np.ascontiguousarray(np.array(x).transpose((0, 3, 1, 2))) # stack and BHWC to BCHW
(0, 3, 1, 2))) # stack and BHWC to BCHW x = torch.from_numpy(x).to(p.device).type_as(p) / 255 # uint8 to fp16/32
x = torch.from_numpy(x).to(p.device).type_as(
p) / 255 # uint8 to fp16/32
with amp.autocast(autocast): with amp.autocast(autocast):
# Inference # Inference
@ -806,8 +723,7 @@ class Detections:
def __init__(self, ims, pred, files, times=(0, 0, 0), names=None, shape=None): def __init__(self, ims, pred, files, times=(0, 0, 0), names=None, shape=None):
super().__init__() super().__init__()
d = pred[0].device # device d = pred[0].device # device
gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) for im in ims] # normalizations
for im in ims] # normalizations
self.ims = ims # list of images as numpy arrays self.ims = ims # list of images as numpy arrays
self.pred = pred # list of tensors pred[0] = (xyxy, conf, cls) self.pred = pred # list of tensors pred[0] = (xyxy, conf, cls)
self.names = names # class names self.names = names # class names
@ -824,23 +740,18 @@ class Detections:
def _run(self, pprint=False, show=False, save=False, crop=False, render=False, labels=True, save_dir=Path('')): def _run(self, pprint=False, show=False, save=False, crop=False, render=False, labels=True, save_dir=Path('')):
s, crops = '', [] s, crops = '', []
for i, (im, pred) in enumerate(zip(self.ims, self.pred)): for i, (im, pred) in enumerate(zip(self.ims, self.pred)):
# string s += f'\nimage {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} ' # string
s += f'\nimage {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} '
if pred.shape[0]: if pred.shape[0]:
for c in pred[:, -1].unique(): for c in pred[:, -1].unique():
n = (pred[:, -1] == c).sum() # detections per class n = (pred[:, -1] == c).sum() # detections per class
# add to string s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, " # add to string
s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, "
s = s.rstrip(', ') s = s.rstrip(', ')
if show or save or render or crop: if show or save or render or crop:
annotator = Annotator(im, example=str(self.names)) annotator = Annotator(im, example=str(self.names))
# xyxy, confidence, class for *box, conf, cls in reversed(pred): # xyxy, confidence, class
for *box, conf, cls in reversed(pred):
label = f'{self.names[int(cls)]} {conf:.2f}' label = f'{self.names[int(cls)]} {conf:.2f}'
if crop: if crop:
file = save_dir / 'crops' / \ file = save_dir / 'crops' / self.names[int(cls)] / self.files[i] if save else None
self.names[int(cls)] / \
self.files[i] if save else None
crops.append({ crops.append({
'box': box, 'box': box,
'conf': conf, 'conf': conf,
@ -848,22 +759,23 @@ class Detections:
'label': label, 'label': label,
'im': save_one_box(box, im, file=file, save=save)}) 'im': save_one_box(box, im, file=file, save=save)})
else: # all others else: # all others
annotator.box_label( annotator.box_label(box, label if labels else '', color=colors(cls))
box, label if labels else '', color=colors(cls))
im = annotator.im im = annotator.im
else: else:
s += '(no detections)' s += '(no detections)'
im = Image.fromarray(im.astype(np.uint8)) if isinstance( im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np
im, np.ndarray) else im # from np
if show: if show:
display(im) if is_notebook() else im.show(self.files[i]) if is_jupyter():
from IPython.display import display
display(im)
else:
im.show(self.files[i])
if save: if save:
f = self.files[i] f = self.files[i]
im.save(save_dir / f) # save im.save(save_dir / f) # save
if i == self.n - 1: if i == self.n - 1:
LOGGER.info( LOGGER.info(f"Saved {self.n} image{'s' * (self.n > 1)} to {colorstr('bold', save_dir)}")
f"Saved {self.n} image{'s' * (self.n > 1)} to {colorstr('bold', save_dir)}")
if render: if render:
self.ims[i] = np.asarray(im) self.ims[i] = np.asarray(im)
if pprint: if pprint:
@ -879,15 +791,12 @@ class Detections:
self._run(show=True, labels=labels) # show results self._run(show=True, labels=labels) # show results
def save(self, labels=True, save_dir='runs/detect/exp', exist_ok=False): def save(self, labels=True, save_dir='runs/detect/exp', exist_ok=False):
save_dir = increment_path( save_dir = increment_path(save_dir, exist_ok, mkdir=True) # increment save_dir
save_dir, exist_ok, mkdir=True) # increment save_dir
self._run(save=True, labels=labels, save_dir=save_dir) # save results self._run(save=True, labels=labels, save_dir=save_dir) # save results
def crop(self, save=True, save_dir='runs/detect/exp', exist_ok=False): def crop(self, save=True, save_dir='runs/detect/exp', exist_ok=False):
save_dir = increment_path( save_dir = increment_path(save_dir, exist_ok, mkdir=True) if save else None
save_dir, exist_ok, mkdir=True) if save else None return self._run(crop=True, save=save, save_dir=save_dir) # crop results
# crop results
return self._run(crop=True, save=save, save_dir=save_dir)
def render(self, labels=True): def render(self, labels=True):
self._run(render=True, labels=labels) # render results self._run(render=True, labels=labels) # render results
@ -899,16 +808,14 @@ class Detections:
ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name' # xyxy columns ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name' # xyxy columns
cb = 'xcenter', 'ycenter', 'width', 'height', 'confidence', 'class', 'name' # xywh columns cb = 'xcenter', 'ycenter', 'width', 'height', 'confidence', 'class', 'name' # xywh columns
for k, c in zip(['xyxy', 'xyxyn', 'xywh', 'xywhn'], [ca, ca, cb, cb]): for k, c in zip(['xyxy', 'xyxyn', 'xywh', 'xywhn'], [ca, ca, cb, cb]):
a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] for x in x.tolist()] for x in getattr(self, k)] # update
for x in x.tolist()] for x in getattr(self, k)] # update
setattr(new, k, [pd.DataFrame(x, columns=c) for x in a]) setattr(new, k, [pd.DataFrame(x, columns=c) for x in a])
return new return new
def tolist(self): def tolist(self):
# return a list of Detections objects, i.e. 'for result in results.tolist():' # return a list of Detections objects, i.e. 'for result in results.tolist():'
r = range(self.n) # iterable r = range(self.n) # iterable
x = [Detections([self.ims[i]], [self.pred[i]], [ x = [Detections([self.ims[i]], [self.pred[i]], [self.files[i]], self.times, self.names, self.s) for i in r]
self.files[i]], self.times, self.names, self.s) for i in r]
# for d in x: # for d in x:
# for k in ['ims', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']: # for k in ['ims', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']:
# setattr(d, k, getattr(d, k)[0]) # pop out of list # setattr(d, k, getattr(d, k)[0]) # pop out of list
@ -942,13 +849,19 @@ class Proto(nn.Module):
class Classify(nn.Module): class Classify(nn.Module):
# YOLOv5 classification head, i.e. x(b,c1,20,20) to x(b,c2) # YOLOv5 classification head, i.e. x(b,c1,20,20) to x(b,c2)
# ch_in, ch_out, kernel, stride, padding, groups def __init__(self,
def __init__(self, c1, c2, k=1, s=1, p=None, g=1): c1,
c2,
k=1,
s=1,
p=None,
g=1,
dropout_p=0.0): # ch_in, ch_out, kernel, stride, padding, groups, dropout probability
super().__init__() super().__init__()
c_ = 1280 # efficientnet_b0 size c_ = 1280 # efficientnet_b0 size
self.conv = Conv(c1, c_, k, s, autopad(k, p), g) self.conv = Conv(c1, c_, k, s, autopad(k, p), g)
self.pool = nn.AdaptiveAvgPool2d(1) # to x(b,c_,1,1) self.pool = nn.AdaptiveAvgPool2d(1) # to x(b,c_,1,1)
self.drop = nn.Dropout(p=0.0, inplace=True) self.drop = nn.Dropout(p=dropout_p, inplace=True)
self.linear = nn.Linear(c_, c2) # to x(b,c2) self.linear = nn.Linear(c_, c2) # to x(b,c2)
def forward(self, x): def forward(self, x):

View File

@ -356,7 +356,7 @@ class TFUpsample(keras.layers.Layer):
# TF version of torch.nn.Upsample() # TF version of torch.nn.Upsample()
def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w' def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w'
super().__init__() super().__init__()
assert scale_factor % 2 == 0, "scale_factor must be multiple of 2" assert scale_factor % 2 == 0, 'scale_factor must be multiple of 2'
self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * scale_factor, x.shape[2] * scale_factor), mode) self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * scale_factor, x.shape[2] * scale_factor), mode)
# self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode) # self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode)
# with default arguments: align_corners=False, half_pixel_centers=False # with default arguments: align_corners=False, half_pixel_centers=False
@ -371,7 +371,7 @@ class TFConcat(keras.layers.Layer):
# TF version of torch.concat() # TF version of torch.concat()
def __init__(self, dimension=1, w=None): def __init__(self, dimension=1, w=None):
super().__init__() super().__init__()
assert dimension == 1, "convert only NCHW to NHWC concat" assert dimension == 1, 'convert only NCHW to NHWC concat'
self.d = 3 self.d = 3
def call(self, inputs): def call(self, inputs):
@ -523,17 +523,17 @@ class AgnosticNMS(keras.layers.Layer):
selected_boxes = tf.gather(boxes, selected_inds) selected_boxes = tf.gather(boxes, selected_inds)
padded_boxes = tf.pad(selected_boxes, padded_boxes = tf.pad(selected_boxes,
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]], paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]],
mode="CONSTANT", mode='CONSTANT',
constant_values=0.0) constant_values=0.0)
selected_scores = tf.gather(scores_inp, selected_inds) selected_scores = tf.gather(scores_inp, selected_inds)
padded_scores = tf.pad(selected_scores, padded_scores = tf.pad(selected_scores,
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
mode="CONSTANT", mode='CONSTANT',
constant_values=-1.0) constant_values=-1.0)
selected_classes = tf.gather(class_inds, selected_inds) selected_classes = tf.gather(class_inds, selected_inds)
padded_classes = tf.pad(selected_classes, padded_classes = tf.pad(selected_classes,
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
mode="CONSTANT", mode='CONSTANT',
constant_values=-1.0) constant_values=-1.0)
valid_detections = tf.shape(selected_inds)[0] valid_detections = tf.shape(selected_inds)[0]
return padded_boxes, padded_scores, padded_classes, valid_detections return padded_boxes, padded_scores, padded_classes, valid_detections
@ -603,6 +603,6 @@ def main(opt):
run(**vars(opt)) run(**vars(opt))
if __name__ == "__main__": if __name__ == '__main__':
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)

View File

@ -3,7 +3,7 @@ PyDirectInput
Pillow Pillow
opencv-python opencv-python
mss mss
numpy numpy==1.23
pandas pandas
pywin32 pywin32
pyyaml pyyaml

View File

@ -60,16 +60,18 @@ def notebook_init(verbose=True):
check_font() check_font()
import psutil import psutil
from IPython import display # to display images and clear console output
if is_colab(): if is_colab():
shutil.rmtree('/content/sample_data', ignore_errors=True) # remove colab /sample_data directory shutil.rmtree('/content/sample_data', ignore_errors=True) # remove colab /sample_data directory
# System info # System info
display = None
if verbose: if verbose:
gb = 1 << 30 # bytes to GiB (1024 ** 3) gb = 1 << 30 # bytes to GiB (1024 ** 3)
ram = psutil.virtual_memory().total ram = psutil.virtual_memory().total
total, used, free = shutil.disk_usage("/") total, used, free = shutil.disk_usage('/')
with contextlib.suppress(Exception): # clear display if ipython is installed
from IPython import display
display.clear_output() display.clear_output()
s = f'({os.cpu_count()} CPUs, {ram / gb:.1f} GB RAM, {(total - free) / gb:.1f}/{total / gb:.1f} GB disk)' s = f'({os.cpu_count()} CPUs, {ram / gb:.1f} GB RAM, {(total - free) / gb:.1f}/{total / gb:.1f} GB disk)'
else: else:

View File

@ -201,7 +201,7 @@ def random_perspective(im,
# Transform label coordinates # Transform label coordinates
n = len(targets) n = len(targets)
if n: if n:
use_segments = any(x.any() for x in segments) use_segments = any(x.any() for x in segments) and len(segments) == n
new = np.zeros((n, 4)) new = np.zeros((n, 4))
if use_segments: # warp segments if use_segments: # warp segments
segments = resample_segments(segments) # upsample segments = resample_segments(segments) # upsample

View File

@ -52,7 +52,7 @@ for orientation in ExifTags.TAGS.keys():
def get_hash(paths): def get_hash(paths):
# Returns a single hash value of a list of paths (files or dirs) # Returns a single hash value of a list of paths (files or dirs)
size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes
h = hashlib.md5(str(size).encode()) # hash sizes h = hashlib.sha256(str(size).encode()) # hash sizes
h.update(''.join(paths).encode()) # hash paths h.update(''.join(paths).encode()) # hash paths
return h.hexdigest() # return hash return h.hexdigest() # return hash
@ -89,7 +89,7 @@ def exif_transpose(image):
if method is not None: if method is not None:
image = image.transpose(method) image = image.transpose(method)
del exif[0x0112] del exif[0x0112]
image.info["exif"] = exif.tobytes() image.info['exif'] = exif.tobytes()
return image return image
@ -212,11 +212,11 @@ class LoadScreenshots:
# Parse monitor shape # Parse monitor shape
monitor = self.sct.monitors[self.screen] monitor = self.sct.monitors[self.screen]
self.top = monitor["top"] if top is None else (monitor["top"] + top) self.top = monitor['top'] if top is None else (monitor['top'] + top)
self.left = monitor["left"] if left is None else (monitor["left"] + left) self.left = monitor['left'] if left is None else (monitor['left'] + left)
self.width = width or monitor["width"] self.width = width or monitor['width']
self.height = height or monitor["height"] self.height = height or monitor['height']
self.monitor = {"left": self.left, "top": self.top, "width": self.width, "height": self.height} self.monitor = {'left': self.left, 'top': self.top, 'width': self.width, 'height': self.height}
def __iter__(self): def __iter__(self):
return self return self
@ -224,7 +224,7 @@ class LoadScreenshots:
def __next__(self): def __next__(self):
# mss screen capture: get raw pixels from the screen as np array # mss screen capture: get raw pixels from the screen as np array
im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR
s = f"screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: " s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: '
if self.transforms: if self.transforms:
im = self.transforms(im0) # transforms im = self.transforms(im0) # transforms
@ -239,7 +239,7 @@ class LoadScreenshots:
class LoadImages: class LoadImages:
# YOLOv5 image/video dataloader, i.e. `python detect.py --source image.jpg/vid.mp4` # YOLOv5 image/video dataloader, i.e. `python detect.py --source image.jpg/vid.mp4`
def __init__(self, path, img_size=640, stride=32, auto=True, transforms=None, vid_stride=1): def __init__(self, path, img_size=640, stride=32, auto=True, transforms=None, vid_stride=1):
if isinstance(path, str) and Path(path).suffix == ".txt": # *.txt file with img/vid/dir on each line if isinstance(path, str) and Path(path).suffix == '.txt': # *.txt file with img/vid/dir on each line
path = Path(path).read_text().rsplit() path = Path(path).read_text().rsplit()
files = [] files = []
for p in sorted(path) if isinstance(path, (list, tuple)) else [path]: for p in sorted(path) if isinstance(path, (list, tuple)) else [path]:
@ -358,7 +358,7 @@ class LoadStreams:
# YouTube format i.e. 'https://www.youtube.com/watch?v=Zgi9g1ksQHc' or 'https://youtu.be/Zgi9g1ksQHc' # YouTube format i.e. 'https://www.youtube.com/watch?v=Zgi9g1ksQHc' or 'https://youtu.be/Zgi9g1ksQHc'
check_requirements(('pafy', 'youtube_dl==2020.12.2')) check_requirements(('pafy', 'youtube_dl==2020.12.2'))
import pafy import pafy
s = pafy.new(s).getbest(preftype="mp4").url # YouTube URL s = pafy.new(s).getbest(preftype='mp4').url # YouTube URL
s = eval(s) if s.isnumeric() else s # i.e. s = '0' local webcam s = eval(s) if s.isnumeric() else s # i.e. s = '0' local webcam
if s == 0: if s == 0:
assert not is_colab(), '--source 0 webcam unsupported on Colab. Rerun command in a local environment.' assert not is_colab(), '--source 0 webcam unsupported on Colab. Rerun command in a local environment.'
@ -373,7 +373,7 @@ class LoadStreams:
_, self.imgs[i] = cap.read() # guarantee first frame _, self.imgs[i] = cap.read() # guarantee first frame
self.threads[i] = Thread(target=self.update, args=([i, cap, s]), daemon=True) self.threads[i] = Thread(target=self.update, args=([i, cap, s]), daemon=True)
LOGGER.info(f"{st} Success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)") LOGGER.info(f'{st} Success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)')
self.threads[i].start() self.threads[i].start()
LOGGER.info('') # newline LOGGER.info('') # newline
@ -495,7 +495,7 @@ class LoadImagesAndLabels(Dataset):
# Display cache # Display cache
nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupt, total nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupt, total
if exists and LOCAL_RANK in {-1, 0}: if exists and LOCAL_RANK in {-1, 0}:
d = f"Scanning {cache_path}... {nf} images, {nm + ne} backgrounds, {nc} corrupt" d = f'Scanning {cache_path}... {nf} images, {nm + ne} backgrounds, {nc} corrupt'
tqdm(None, desc=prefix + d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT) # display cache results tqdm(None, desc=prefix + d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT) # display cache results
if cache['msgs']: if cache['msgs']:
LOGGER.info('\n'.join(cache['msgs'])) # display warnings LOGGER.info('\n'.join(cache['msgs'])) # display warnings
@ -598,8 +598,8 @@ class LoadImagesAndLabels(Dataset):
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
cache = mem_required * (1 + safety_margin) < mem.available # to cache or not to cache, that is the question cache = mem_required * (1 + safety_margin) < mem.available # to cache or not to cache, that is the question
if not cache: if not cache:
LOGGER.info(f"{prefix}{mem_required / gb:.1f}GB RAM required, " LOGGER.info(f'{prefix}{mem_required / gb:.1f}GB RAM required, '
f"{mem.available / gb:.1f}/{mem.total / gb:.1f}GB available, " f'{mem.available / gb:.1f}/{mem.total / gb:.1f}GB available, '
f"{'caching images ✅' if cache else 'not caching images ⚠️'}") f"{'caching images ✅' if cache else 'not caching images ⚠️'}")
return cache return cache
@ -607,7 +607,7 @@ class LoadImagesAndLabels(Dataset):
# Cache dataset labels, check images and read shapes # Cache dataset labels, check images and read shapes
x = {} # dict x = {} # dict
nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages
desc = f"{prefix}Scanning {path.parent / path.stem}..." desc = f'{prefix}Scanning {path.parent / path.stem}...'
with Pool(NUM_THREADS) as pool: with Pool(NUM_THREADS) as pool:
pbar = tqdm(pool.imap(verify_image_label, zip(self.im_files, self.label_files, repeat(prefix))), pbar = tqdm(pool.imap(verify_image_label, zip(self.im_files, self.label_files, repeat(prefix))),
desc=desc, desc=desc,
@ -622,7 +622,7 @@ class LoadImagesAndLabels(Dataset):
x[im_file] = [lb, shape, segments] x[im_file] = [lb, shape, segments]
if msg: if msg:
msgs.append(msg) msgs.append(msg)
pbar.desc = f"{desc} {nf} images, {nm + ne} backgrounds, {nc} corrupt" pbar.desc = f'{desc} {nf} images, {nm + ne} backgrounds, {nc} corrupt'
pbar.close() pbar.close()
if msgs: if msgs:
@ -1063,7 +1063,7 @@ class HUBDatasetStats():
if zipped: if zipped:
data['path'] = data_dir data['path'] = data_dir
except Exception as e: except Exception as e:
raise Exception("error/HUB/dataset_stats/yaml_load") from e raise Exception('error/HUB/dataset_stats/yaml_load') from e
check_dataset(data, autodownload) # download dataset if missing check_dataset(data, autodownload) # download dataset if missing
self.hub_dir = Path(data['path'] + '-hub') self.hub_dir = Path(data['path'] + '-hub')
@ -1188,7 +1188,7 @@ class ClassificationDataset(torchvision.datasets.ImageFolder):
else: # read image else: # read image
im = cv2.imread(f) # BGR im = cv2.imread(f) # BGR
if self.album_transforms: if self.album_transforms:
sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))["image"] sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))['image']
else: else:
sample = self.torch_transforms(im) sample = self.torch_transforms(im)
return sample, j return sample, j

View File

@ -3,35 +3,44 @@
# Image is CUDA-optimized for YOLOv5 single/multi-GPU training and inference # Image is CUDA-optimized for YOLOv5 single/multi-GPU training and inference
# Start FROM NVIDIA PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch # Start FROM NVIDIA PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch
FROM nvcr.io/nvidia/pytorch:22.12-py3 # FROM docker.io/pytorch/pytorch:latest
RUN rm -rf /opt/pytorch # remove 1.2GB dir FROM pytorch/pytorch:latest
# Downloads to user config dir # Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/ ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
# Install linux packages # Install linux packages
RUN apt update && apt install --no-install-recommends -y zip htop screen libgl1-mesa-glx ENV DEBIAN_FRONTEND noninteractive
RUN apt update
RUN TZ=Etc/UTC apt install -y tzdata
RUN apt install --no-install-recommends -y gcc git zip curl htop libgl1-mesa-glx libglib2.0-0 libpython3-dev gnupg
# RUN alias python=python3
# Install pip packages (uninstall torch nightly in favor of stable) # Security updates
COPY requirements.txt . # https://security.snyk.io/vuln/SNYK-UBUNTU1804-OPENSSL-3314796
RUN python -m pip install --upgrade pip wheel RUN apt upgrade --no-install-recommends -y openssl
RUN pip uninstall -y Pillow torchtext torch torchvision
RUN pip install --no-cache -U pycocotools # install --upgrade
RUN pip install --no-cache -r requirements.txt albumentations comet gsutil notebook 'opencv-python<4.6.0.66' \
Pillow>=9.1.0 ultralytics \
--extra-index-url https://download.pytorch.org/whl/cu113
# Create working directory # Create working directory
RUN mkdir -p /usr/src/app RUN rm -rf /usr/src/app && mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Copy contents # Copy contents
# COPY . /usr/src/app (issues as not a .git directory) # COPY . /usr/src/app (issues as not a .git directory)
RUN git clone https://github.com/ultralytics/yolov5 /usr/src/app RUN git clone https://github.com/ultralytics/yolov5 /usr/src/app
# Install pip packages
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip wheel
RUN pip install --no-cache -r requirements.txt albumentations comet gsutil notebook \
coremltools onnx onnx-simplifier onnxruntime 'openvino-dev>=2022.3'
# tensorflow tensorflowjs \
# Set environment variables # Set environment variables
ENV OMP_NUM_THREADS=1 ENV OMP_NUM_THREADS=1
# Cleanup
ENV DEBIAN_FRONTEND teletype
# Usage Examples ------------------------------------------------------------------------------------------------------- # Usage Examples -------------------------------------------------------------------------------------------------------
@ -54,7 +63,7 @@ ENV OMP_NUM_THREADS=1
# t=ultralytics/yolov5:latest tnew=ultralytics/yolov5:v6.2 && sudo docker pull $t && sudo docker tag $t $tnew && sudo docker push $tnew # t=ultralytics/yolov5:latest tnew=ultralytics/yolov5:v6.2 && sudo docker pull $t && sudo docker tag $t $tnew && sudo docker push $tnew
# Clean up # Clean up
# docker system prune -a --volumes # sudo docker system prune -a --volumes
# Update Ubuntu drivers # Update Ubuntu drivers
# https://www.maketecheasier.com/install-nvidia-drivers-ubuntu/ # https://www.maketecheasier.com/install-nvidia-drivers-ubuntu/

View File

@ -3,7 +3,7 @@
# Image is aarch64-compatible for Apple M1 and other ARM architectures i.e. Jetson Nano and Raspberry Pi # Image is aarch64-compatible for Apple M1 and other ARM architectures i.e. Jetson Nano and Raspberry Pi
# Start FROM Ubuntu image https://hub.docker.com/_/ubuntu # Start FROM Ubuntu image https://hub.docker.com/_/ubuntu
FROM arm64v8/ubuntu:20.04 FROM arm64v8/ubuntu:rolling
# Downloads to user config dir # Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/ ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
@ -18,11 +18,9 @@ RUN apt install --no-install-recommends -y python3-pip git zip curl htop gcc lib
# Install pip packages # Install pip packages
COPY requirements.txt . COPY requirements.txt .
RUN python3 -m pip install --upgrade pip wheel RUN python3 -m pip install --upgrade pip wheel
RUN pip install --no-cache -r requirements.txt ultralytics gsutil notebook \ RUN pip install --no-cache -r requirements.txt albumentations gsutil notebook \
tensorflow-aarch64 coremltools onnx onnxruntime
# tensorflowjs \ # tensorflow-aarch64 tensorflowjs \
# onnx onnx-simplifier onnxruntime \
# coremltools openvino-dev \
# Create working directory # Create working directory
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app

View File

@ -3,7 +3,7 @@
# Image is CPU-optimized for ONNX, OpenVINO and PyTorch YOLOv5 deployments # Image is CPU-optimized for ONNX, OpenVINO and PyTorch YOLOv5 deployments
# Start FROM Ubuntu image https://hub.docker.com/_/ubuntu # Start FROM Ubuntu image https://hub.docker.com/_/ubuntu
FROM ubuntu:20.04 FROM ubuntu:rolling
# Downloads to user config dir # Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/ ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
@ -18,9 +18,9 @@ RUN apt install --no-install-recommends -y python3-pip git zip curl htop libgl1-
# Install pip packages # Install pip packages
COPY requirements.txt . COPY requirements.txt .
RUN python3 -m pip install --upgrade pip wheel RUN python3 -m pip install --upgrade pip wheel
RUN pip install --no-cache -r requirements.txt ultralytics albumentations gsutil notebook \ RUN pip install --no-cache -r requirements.txt albumentations gsutil notebook \
coremltools onnx onnx-simplifier onnxruntime tensorflow-cpu tensorflowjs \ coremltools onnx onnx-simplifier onnxruntime 'openvino-dev>=2022.3' \
# openvino-dev \ # tensorflow tensorflowjs \
--extra-index-url https://download.pytorch.org/whl/cpu --extra-index-url https://download.pytorch.org/whl/cpu
# Create working directory # Create working directory

View File

@ -26,8 +26,10 @@ def is_url(url, check=True):
def gsutil_getsize(url=''): def gsutil_getsize(url=''):
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du # gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
s = subprocess.check_output(f'gsutil du {url}', shell=True).decode('utf-8') output = subprocess.check_output(['gsutil', 'du', url], shell=True, encoding='utf-8')
return eval(s.split(' ')[0]) if len(s) else 0 # bytes if output:
return int(output.split()[0])
return 0
def url_getsize(url='https://ultralytics.com/images/bus.jpg'): def url_getsize(url='https://ultralytics.com/images/bus.jpg'):
@ -36,6 +38,25 @@ def url_getsize(url='https://ultralytics.com/images/bus.jpg'):
return int(response.headers.get('content-length', -1)) return int(response.headers.get('content-length', -1))
def curl_download(url, filename, *, silent: bool = False) -> bool:
"""
Download a file from a url to a filename using curl.
"""
silent_option = 'sS' if silent else '' # silent
proc = subprocess.run([
'curl',
'-#',
f'-{silent_option}L',
url,
'--output',
filename,
'--retry',
'9',
'-C',
'-',])
return proc.returncode == 0
def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''): def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''):
# Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes # Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes
from utils.general import LOGGER from utils.general import LOGGER
@ -50,12 +71,13 @@ def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''):
if file.exists(): if file.exists():
file.unlink() # remove partial downloads file.unlink() # remove partial downloads
LOGGER.info(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...') LOGGER.info(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...')
os.system(f"curl -# -L '{url2 or url}' -o '{file}' --retry 3 -C -") # curl download, retry and resume on fail # curl download, retry and resume on fail
curl_download(url2 or url, file)
finally: finally:
if not file.exists() or file.stat().st_size < min_bytes: # check if not file.exists() or file.stat().st_size < min_bytes: # check
if file.exists(): if file.exists():
file.unlink() # remove partial downloads file.unlink() # remove partial downloads
LOGGER.info(f"ERROR: {assert_msg}\n{error_msg}") LOGGER.info(f'ERROR: {assert_msg}\n{error_msg}')
LOGGER.info('') LOGGER.info('')
@ -98,11 +120,9 @@ def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'):
file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required) file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required)
if name in assets: if name in assets:
url3 = 'https://drive.google.com/drive/folders/1EFQTEUeXWSFww0luse2jB9M1QNZQGwNl' # backup gdrive mirror safe_download(file,
safe_download(
file,
url=f'https://github.com/{repo}/releases/download/{tag}/{name}', url=f'https://github.com/{repo}/releases/download/{tag}/{name}',
min_bytes=1E5, min_bytes=1E5,
error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag} or {url3}') error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag}')
return str(file) return str(file)

View File

@ -7,13 +7,13 @@ import pprint
import requests import requests
DETECTION_URL = "http://localhost:5000/v1/object-detection/yolov5s" DETECTION_URL = 'http://localhost:5000/v1/object-detection/yolov5s'
IMAGE = "zidane.jpg" IMAGE = 'zidane.jpg'
# Read image # Read image
with open(IMAGE, "rb") as f: with open(IMAGE, 'rb') as f:
image_data = f.read() image_data = f.read()
response = requests.post(DETECTION_URL, files={"image": image_data}).json() response = requests.post(DETECTION_URL, files={'image': image_data}).json()
pprint.pprint(response) pprint.pprint(response)

View File

@ -13,36 +13,36 @@ from PIL import Image
app = Flask(__name__) app = Flask(__name__)
models = {} models = {}
DETECTION_URL = "/v1/object-detection/<model>" DETECTION_URL = '/v1/object-detection/<model>'
@app.route(DETECTION_URL, methods=["POST"]) @app.route(DETECTION_URL, methods=['POST'])
def predict(model): def predict(model):
if request.method != "POST": if request.method != 'POST':
return return
if request.files.get("image"): if request.files.get('image'):
# Method 1 # Method 1
# with request.files["image"] as f: # with request.files["image"] as f:
# im = Image.open(io.BytesIO(f.read())) # im = Image.open(io.BytesIO(f.read()))
# Method 2 # Method 2
im_file = request.files["image"] im_file = request.files['image']
im_bytes = im_file.read() im_bytes = im_file.read()
im = Image.open(io.BytesIO(im_bytes)) im = Image.open(io.BytesIO(im_bytes))
if model in models: if model in models:
results = models[model](im, size=640) # reduce size=320 for faster inference results = models[model](im, size=640) # reduce size=320 for faster inference
return results.pandas().xyxy[0].to_json(orient="records") return results.pandas().xyxy[0].to_json(orient='records')
if __name__ == "__main__": if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Flask API exposing YOLOv5 model") parser = argparse.ArgumentParser(description='Flask API exposing YOLOv5 model')
parser.add_argument("--port", default=5000, type=int, help="port number") parser.add_argument('--port', default=5000, type=int, help='port number')
parser.add_argument('--model', nargs='+', default=['yolov5s'], help='model(s) to run, i.e. --model yolov5n yolov5s') parser.add_argument('--model', nargs='+', default=['yolov5s'], help='model(s) to run, i.e. --model yolov5n yolov5s')
opt = parser.parse_args() opt = parser.parse_args()
for m in opt.model: for m in opt.model:
models[m] = torch.hub.load("ultralytics/yolov5", m, force_reload=True, skip_validation=True) models[m] = torch.hub.load('ultralytics/yolov5', m, force_reload=True, skip_validation=True)
app.run(host="0.0.0.0", port=opt.port) # debug=True causes Restarting with stat app.run(host='0.0.0.0', port=opt.port) # debug=True causes Restarting with stat

View File

@ -14,6 +14,7 @@ import platform
import random import random
import re import re
import signal import signal
import subprocess
import sys import sys
import time import time
import urllib import urllib
@ -28,7 +29,6 @@ from typing import Optional
from zipfile import ZipFile, is_zipfile from zipfile import ZipFile, is_zipfile
import cv2 import cv2
import IPython
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pkg_resources as pkg import pkg_resources as pkg
@ -37,7 +37,7 @@ import torchvision
import yaml import yaml
from utils import TryExcept, emojis from utils import TryExcept, emojis
from utils.downloads import gsutil_getsize from utils.downloads import curl_download, gsutil_getsize
from utils.metrics import box_iou, fitness from utils.metrics import box_iou, fitness
FILE = Path(__file__).resolve() FILE = Path(__file__).resolve()
@ -76,10 +76,18 @@ def is_colab():
return 'google.colab' in sys.modules return 'google.colab' in sys.modules
def is_notebook(): def is_jupyter():
# Is environment a Jupyter notebook? Verified on Colab, Jupyterlab, Kaggle, Paperspace """
ipython_type = str(type(IPython.get_ipython())) Check if the current script is running inside a Jupyter Notebook.
return 'colab' in ipython_type or 'zmqshell' in ipython_type Verified on Colab, Jupyterlab, Kaggle, Paperspace.
Returns:
bool: True if running inside a Jupyter Notebook, False otherwise.
"""
with contextlib.suppress(Exception):
from IPython import get_ipython
return get_ipython() is not None
return False
def is_kaggle(): def is_kaggle():
@ -89,11 +97,11 @@ def is_kaggle():
def is_docker() -> bool: def is_docker() -> bool:
"""Check if the process runs inside a docker container.""" """Check if the process runs inside a docker container."""
if Path("/.dockerenv").exists(): if Path('/.dockerenv').exists():
return True return True
try: # check if docker is in control groups try: # check if docker is in control groups
with open("/proc/self/cgroup") as file: with open('/proc/self/cgroup') as file:
return any("docker" in line for line in file) return any('docker' in line for line in file)
except OSError: except OSError:
return False return False
@ -112,7 +120,7 @@ def is_writeable(dir, test=False):
return False return False
LOGGING_NAME = "yolov5" LOGGING_NAME = 'yolov5'
def set_logging(name=LOGGING_NAME, verbose=True): def set_logging(name=LOGGING_NAME, verbose=True):
@ -120,21 +128,21 @@ def set_logging(name=LOGGING_NAME, verbose=True):
rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings
level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR
logging.config.dictConfig({ logging.config.dictConfig({
"version": 1, 'version': 1,
"disable_existing_loggers": False, 'disable_existing_loggers': False,
"formatters": { 'formatters': {
name: { name: {
"format": "%(message)s"}}, 'format': '%(message)s'}},
"handlers": { 'handlers': {
name: { name: {
"class": "logging.StreamHandler", 'class': 'logging.StreamHandler',
"formatter": name, 'formatter': name,
"level": level,}}, 'level': level,}},
"loggers": { 'loggers': {
name: { name: {
"level": level, 'level': level,
"handlers": [name], 'handlers': [name],
"propagate": False,}}}) 'propagate': False,}}})
set_logging(LOGGING_NAME) # run before defining LOGGER set_logging(LOGGING_NAME) # run before defining LOGGER
@ -217,7 +225,7 @@ class WorkingDirectory(contextlib.ContextDecorator):
def methods(instance): def methods(instance):
# Get class/instance methods # Get class/instance methods
return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith("__")] return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith('__')]
def print_args(args: Optional[dict] = None, show_file=True, show_func=False): def print_args(args: Optional[dict] = None, show_file=True, show_func=False):
@ -298,7 +306,7 @@ def check_online():
def run_once(): def run_once():
# Check once # Check once
try: try:
socket.create_connection(("1.1.1.1", 443), 5) # check host accessibility socket.create_connection(('1.1.1.1', 443), 5) # check host accessibility
return True return True
except OSError: except OSError:
return False return False
@ -337,7 +345,7 @@ def check_git_status(repo='ultralytics/yolov5', branch='master'):
n = int(check_output(f'git rev-list {local_branch}..{remote}/{branch} --count', shell=True)) # commits behind n = int(check_output(f'git rev-list {local_branch}..{remote}/{branch} --count', shell=True)) # commits behind
if n > 0: if n > 0:
pull = 'git pull' if remote == 'origin' else f'git pull {remote} {branch}' pull = 'git pull' if remote == 'origin' else f'git pull {remote} {branch}'
s += f"⚠️ YOLOv5 is out of date by {n} commit{'s' * (n > 1)}. Use `{pull}` or `git clone {url}` to update." s += f"⚠️ YOLOv5 is out of date by {n} commit{'s' * (n > 1)}. Use '{pull}' or 'git clone {url}' to update."
else: else:
s += f'up to date with {url}' s += f'up to date with {url}'
LOGGER.info(s) LOGGER.info(s)
@ -385,7 +393,7 @@ def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), insta
check_python() # check python version check_python() # check python version
if isinstance(requirements, Path): # requirements.txt file if isinstance(requirements, Path): # requirements.txt file
file = requirements.resolve() file = requirements.resolve()
assert file.exists(), f"{prefix} {file} not found, check failed." assert file.exists(), f'{prefix} {file} not found, check failed.'
with file.open() as f: with file.open() as f:
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude] requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude]
elif isinstance(requirements, str): elif isinstance(requirements, str):
@ -428,7 +436,7 @@ def check_img_size(imgsz, s=32, floor=0):
def check_imshow(warn=False): def check_imshow(warn=False):
# Check if environment supports image displays # Check if environment supports image displays
try: try:
assert not is_notebook() assert not is_jupyter()
assert not is_docker() assert not is_docker()
cv2.imshow('test', np.zeros((1, 1, 3))) cv2.imshow('test', np.zeros((1, 1, 3)))
cv2.waitKey(1) cv2.waitKey(1)
@ -449,7 +457,7 @@ def check_suffix(file='yolov5s.pt', suffix=('.pt',), msg=''):
for f in file if isinstance(file, (list, tuple)) else [file]: for f in file if isinstance(file, (list, tuple)) else [file]:
s = Path(f).suffix.lower() # file suffix s = Path(f).suffix.lower() # file suffix
if len(s): if len(s):
assert s in suffix, f"{msg}{f} acceptable suffix is {suffix}" assert s in suffix, f'{msg}{f} acceptable suffix is {suffix}'
def check_yaml(file, suffix=('.yaml', '.yml')): def check_yaml(file, suffix=('.yaml', '.yml')):
@ -551,12 +559,12 @@ def check_dataset(data, autodownload=True):
r = None # success r = None # success
elif s.startswith('bash '): # bash script elif s.startswith('bash '): # bash script
LOGGER.info(f'Running {s} ...') LOGGER.info(f'Running {s} ...')
r = os.system(s) r = subprocess.run(s, shell=True)
else: # python script else: # python script
r = exec(s, {'yaml': data}) # return None r = exec(s, {'yaml': data}) # return None
dt = f'({round(time.time() - t, 1)}s)' dt = f'({round(time.time() - t, 1)}s)'
s = f"success ✅ {dt}, saved to {colorstr('bold', DATASETS_DIR)}" if r in (0, None) else f"failure {dt}" s = f"success ✅ {dt}, saved to {colorstr('bold', DATASETS_DIR)}" if r in (0, None) else f'failure {dt}'
LOGGER.info(f"Dataset download {s}") LOGGER.info(f'Dataset download {s}')
check_font('Arial.ttf' if is_ascii(data['names']) else 'Arial.Unicode.ttf', progress=True) # download fonts check_font('Arial.ttf' if is_ascii(data['names']) else 'Arial.Unicode.ttf', progress=True) # download fonts
return data # dictionary return data # dictionary
@ -629,10 +637,7 @@ def download(url, dir='.', unzip=True, delete=True, curl=False, threads=1, retry
LOGGER.info(f'Downloading {url} to {f}...') LOGGER.info(f'Downloading {url} to {f}...')
for i in range(retry + 1): for i in range(retry + 1):
if curl: if curl:
s = 'sS' if threads > 1 else '' # silent success = curl_download(url, f, silent=(threads > 1))
r = os.system(
f'curl -# -{s}L "{url}" -o "{f}" --retry 9 -C -') # curl download with retry, continue
success = r == 0
else: else:
torch.hub.download_url_to_file(url, f, progress=threads == 1) # torch download torch.hub.download_url_to_file(url, f, progress=threads == 1) # torch download
success = f.is_file() success = f.is_file()
@ -648,9 +653,9 @@ def download(url, dir='.', unzip=True, delete=True, curl=False, threads=1, retry
if is_zipfile(f): if is_zipfile(f):
unzip_file(f, dir) # unzip unzip_file(f, dir) # unzip
elif is_tarfile(f): elif is_tarfile(f):
os.system(f'tar xf {f} --directory {f.parent}') # unzip subprocess.run(['tar', 'xf', f, '--directory', f.parent], check=True) # unzip
elif f.suffix == '.gz': elif f.suffix == '.gz':
os.system(f'tar xfz {f} --directory {f.parent}') # unzip subprocess.run(['tar', 'xfz', f, '--directory', f.parent], check=True) # unzip
if delete: if delete:
f.unlink() # remove zip f.unlink() # remove zip
@ -675,7 +680,7 @@ def make_divisible(x, divisor):
def clean_str(s): def clean_str(s):
# Cleans a string by replacing special characters with underscore _ # Cleans a string by replacing special characters with underscore _
return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s) return re.sub(pattern='[|@#!¡·$€%&()=?¿^*;:,¨´><+]', repl='_', string=s)
def one_cycle(y1=0.0, y2=1.0, steps=100): def one_cycle(y1=0.0, y2=1.0, steps=100):
@ -1022,7 +1027,7 @@ def print_mutation(keys, results, hyp, save_dir, bucket, prefix=colorstr('evolve
if bucket: if bucket:
url = f'gs://{bucket}/evolve.csv' url = f'gs://{bucket}/evolve.csv'
if gsutil_getsize(url) > (evolve_csv.stat().st_size if evolve_csv.exists() else 0): if gsutil_getsize(url) > (evolve_csv.stat().st_size if evolve_csv.exists() else 0):
os.system(f'gsutil cp {url} {save_dir}') # download evolve.csv if larger than local subprocess.run(['gsutil', 'cp', f'{url}', f'{save_dir}']) # download evolve.csv if larger than local
# Log to evolve.csv # Log to evolve.csv
s = '' if evolve_csv.exists() else (('%20s,' * n % keys).rstrip(',') + '\n') # add header s = '' if evolve_csv.exists() else (('%20s,' * n % keys).rstrip(',') + '\n') # add header
@ -1046,7 +1051,7 @@ def print_mutation(keys, results, hyp, save_dir, bucket, prefix=colorstr('evolve
for x in vals) + '\n\n') for x in vals) + '\n\n')
if bucket: if bucket:
os.system(f'gsutil cp {evolve_csv} {evolve_yaml} gs://{bucket}') # upload subprocess.run(['gsutil', 'cp', f'{evolve_csv}', f'{evolve_yaml}', f'gs://{bucket}']) # upload
def apply_classifier(x, model, img, im0): def apply_classifier(x, model, img, im0):

View File

@ -1,4 +1,5 @@
# add these requirements in your app on top of the existing ones # add these requirements in your app on top of the existing ones
pip==21.1 pip==21.1
Flask==1.0.2 Flask==1.0.2
gunicorn==19.9.0 gunicorn==19.10.0
werkzeug>=2.2.3 # not directly required, pinned by Snyk to avoid a vulnerability

View File

@ -84,10 +84,6 @@ class Loggers():
self.csv = True # always log to csv self.csv = True # always log to csv
# Messages # Messages
# if not wandb:
# prefix = colorstr('Weights & Biases: ')
# s = f"{prefix}run 'pip install wandb' to automatically track and visualize YOLOv5 🚀 runs in Weights & Biases"
# self.logger.info(s)
if not clearml: if not clearml:
prefix = colorstr('ClearML: ') prefix = colorstr('ClearML: ')
s = f"{prefix}run 'pip install clearml' to automatically track, visualize and remotely train YOLOv5 🚀 in ClearML" s = f"{prefix}run 'pip install clearml' to automatically track, visualize and remotely train YOLOv5 🚀 in ClearML"
@ -105,14 +101,8 @@ class Loggers():
# W&B # W&B
if wandb and 'wandb' in self.include: if wandb and 'wandb' in self.include:
wandb_artifact_resume = isinstance(self.opt.resume, str) and self.opt.resume.startswith('wandb-artifact://')
run_id = torch.load(self.weights).get('wandb_id') if self.opt.resume and not wandb_artifact_resume else None
self.opt.hyp = self.hyp # add hyperparameters self.opt.hyp = self.hyp # add hyperparameters
self.wandb = WandbLogger(self.opt, run_id) self.wandb = WandbLogger(self.opt)
# temp warn. because nested artifacts not supported after 0.12.10
# if pkg.parse_version(wandb.__version__) >= pkg.parse_version('0.12.11'):
# s = "YOLOv5 temporarily requires wandb version 0.12.10 or below. Some features may not work as expected."
# self.logger.warning(s)
else: else:
self.wandb = None self.wandb = None
@ -131,8 +121,8 @@ class Loggers():
# Comet # Comet
if comet_ml and 'comet' in self.include: if comet_ml and 'comet' in self.include:
if isinstance(self.opt.resume, str) and self.opt.resume.startswith("comet://"): if isinstance(self.opt.resume, str) and self.opt.resume.startswith('comet://'):
run_id = self.opt.resume.split("/")[-1] run_id = self.opt.resume.split('/')[-1]
self.comet_logger = CometLogger(self.opt, self.hyp, run_id=run_id) self.comet_logger = CometLogger(self.opt, self.hyp, run_id=run_id)
else: else:
@ -168,14 +158,14 @@ class Loggers():
plot_labels(labels, names, self.save_dir) plot_labels(labels, names, self.save_dir)
paths = self.save_dir.glob('*labels*.jpg') # training labels paths = self.save_dir.glob('*labels*.jpg') # training labels
if self.wandb: if self.wandb:
self.wandb.log({"Labels": [wandb.Image(str(x), caption=x.name) for x in paths]}) self.wandb.log({'Labels': [wandb.Image(str(x), caption=x.name) for x in paths]})
# if self.clearml: # if self.clearml:
# pass # ClearML saves these images automatically using hooks # pass # ClearML saves these images automatically using hooks
if self.comet_logger: if self.comet_logger:
self.comet_logger.on_pretrain_routine_end(paths) self.comet_logger.on_pretrain_routine_end(paths)
def on_train_batch_end(self, model, ni, imgs, targets, paths, vals): def on_train_batch_end(self, model, ni, imgs, targets, paths, vals):
log_dict = dict(zip(self.keys[0:3], vals)) log_dict = dict(zip(self.keys[:3], vals))
# Callback runs on train batch end # Callback runs on train batch end
# ni: number integrated batches (since train start) # ni: number integrated batches (since train start)
if self.plots: if self.plots:
@ -222,7 +212,7 @@ class Loggers():
if self.wandb or self.clearml: if self.wandb or self.clearml:
files = sorted(self.save_dir.glob('val*.jpg')) files = sorted(self.save_dir.glob('val*.jpg'))
if self.wandb: if self.wandb:
self.wandb.log({"Validation": [wandb.Image(str(f), caption=f.name) for f in files]}) self.wandb.log({'Validation': [wandb.Image(str(f), caption=f.name) for f in files]})
if self.clearml: if self.clearml:
self.clearml.log_debug_samples(files, title='Validation') self.clearml.log_debug_samples(files, title='Validation')
@ -253,7 +243,7 @@ class Loggers():
for i, name in enumerate(self.best_keys): for i, name in enumerate(self.best_keys):
self.wandb.wandb_run.summary[name] = best_results[i] # log best results in the summary self.wandb.wandb_run.summary[name] = best_results[i] # log best results in the summary
self.wandb.log(x) self.wandb.log(x)
self.wandb.end_epoch(best_result=best_fitness == fi) self.wandb.end_epoch()
if self.clearml: if self.clearml:
self.clearml.current_epoch_logged_images = set() # reset epoch image limit self.clearml.current_epoch_logged_images = set() # reset epoch image limit
@ -289,7 +279,7 @@ class Loggers():
if self.wandb: if self.wandb:
self.wandb.log(dict(zip(self.keys[3:10], results))) self.wandb.log(dict(zip(self.keys[3:10], results)))
self.wandb.log({"Results": [wandb.Image(str(f), caption=f.name) for f in files]}) self.wandb.log({'Results': [wandb.Image(str(f), caption=f.name) for f in files]})
# Calling wandb.log. TODO: Refactor this into WandbLogger.log_model # Calling wandb.log. TODO: Refactor this into WandbLogger.log_model
if not self.opt.evolve: if not self.opt.evolve:
wandb.log_artifact(str(best if best.exists() else last), wandb.log_artifact(str(best if best.exists() else last),
@ -339,7 +329,7 @@ class GenericLogger:
if wandb and 'wandb' in self.include: if wandb and 'wandb' in self.include:
self.wandb = wandb.init(project=web_project_name(str(opt.project)), self.wandb = wandb.init(project=web_project_name(str(opt.project)),
name=None if opt.name == "exp" else opt.name, name=None if opt.name == 'exp' else opt.name,
config=opt) config=opt)
else: else:
self.wandb = None self.wandb = None
@ -380,12 +370,12 @@ class GenericLogger:
def log_model(self, model_path, epoch=0, metadata={}): def log_model(self, model_path, epoch=0, metadata={}):
# Log model to all loggers # Log model to all loggers
if self.wandb: if self.wandb:
art = wandb.Artifact(name=f"run_{wandb.run.id}_model", type="model", metadata=metadata) art = wandb.Artifact(name=f'run_{wandb.run.id}_model', type='model', metadata=metadata)
art.add_file(str(model_path)) art.add_file(str(model_path))
wandb.log_artifact(art) wandb.log_artifact(art)
def update_params(self, params): def update_params(self, params):
# Update the paramters logged # Update the parameters logged
if self.wandb: if self.wandb:
wandb.run.config.update(params, allow_val_change=True) wandb.run.config.update(params, allow_val_change=True)

View File

@ -23,7 +23,6 @@ And so much more. It's up to you how many of these tools you want to use, you ca
![ClearML scalars dashboard](https://github.com/thepycoder/clearml_screenshots/raw/main/experiment_manager_with_compare.gif) ![ClearML scalars dashboard](https://github.com/thepycoder/clearml_screenshots/raw/main/experiment_manager_with_compare.gif)
<br /> <br />
<br /> <br />
@ -60,18 +59,20 @@ pip install clearml>=1.2.0
This will enable integration with the YOLOv5 training script. Every training run from now on, will be captured and stored by the ClearML experiment manager. This will enable integration with the YOLOv5 training script. Every training run from now on, will be captured and stored by the ClearML experiment manager.
If you want to change the `project_name` or `task_name`, use the `--project` and `--name` arguments of the `train.py` script, by default the project will be called `YOLOv5` and the task `Training`. If you want to change the `project_name` or `task_name`, use the `--project` and `--name` arguments of the `train.py` script, by default the project will be called `YOLOv5` and the task `Training`.
PLEASE NOTE: ClearML uses `/` as a delimter for subprojects, so be careful when using `/` in your project name! PLEASE NOTE: ClearML uses `/` as a delimiter for subprojects, so be careful when using `/` in your project name!
```bash ```bash
python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache
``` ```
or with custom project and task name: or with custom project and task name:
```bash ```bash
python train.py --project my_project --name my_training --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache python train.py --project my_project --name my_training --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache
``` ```
This will capture: This will capture:
- Source code + uncommitted changes - Source code + uncommitted changes
- Installed packages - Installed packages
- (Hyper)parameters - (Hyper)parameters
@ -94,7 +95,7 @@ There even more we can do with all of this information, like hyperparameter opti
## 🔗 Dataset Version Management ## 🔗 Dataset Version Management
Versioning your data separately from your code is generally a good idea and makes it easy to aqcuire the latest version too. This repository supports supplying a dataset version ID and it will make sure to get the data if it's not there yet. Next to that, this workflow also saves the used dataset ID as part of the task parameters, so you will always know for sure which data was used in which experiment! Versioning your data separately from your code is generally a good idea and makes it easy to acquire the latest version too. This repository supports supplying a dataset version ID, and it will make sure to get the data if it's not there yet. Next to that, this workflow also saves the used dataset ID as part of the task parameters, so you will always know for sure which data was used in which experiment!
![ClearML Dataset Interface](https://github.com/thepycoder/clearml_screenshots/raw/main/clearml_data.gif) ![ClearML Dataset Interface](https://github.com/thepycoder/clearml_screenshots/raw/main/clearml_data.gif)
@ -112,6 +113,7 @@ The YOLOv5 repository supports a number of different datasets by using yaml file
|_ LICENSE |_ LICENSE
|_ README.txt |_ README.txt
``` ```
But this can be any dataset you wish. Feel free to use your own, as long as you keep to this folder structure. But this can be any dataset you wish. Feel free to use your own, as long as you keep to this folder structure.
Next, ⚠️**copy the corresponding yaml file to the root of the dataset folder**⚠️. This yaml files contains the information ClearML will need to properly use the dataset. You can make this yourself too, of course, just follow the structure of the example yamls. Next, ⚠️**copy the corresponding yaml file to the root of the dataset folder**⚠️. This yaml files contains the information ClearML will need to properly use the dataset. You can make this yourself too, of course, just follow the structure of the example yamls.
@ -132,13 +134,15 @@ Basically we need the following keys: `path`, `train`, `test`, `val`, `nc`, `nam
### Upload Your Dataset ### Upload Your Dataset
To get this dataset into ClearML as a versionned dataset, go to the dataset root folder and run the following command: To get this dataset into ClearML as a versioned dataset, go to the dataset root folder and run the following command:
```bash ```bash
cd coco128 cd coco128
clearml-data sync --project YOLOv5 --name coco128 --folder . clearml-data sync --project YOLOv5 --name coco128 --folder .
``` ```
The command `clearml-data sync` is actually a shorthand command. You could also run these commands one after the other: The command `clearml-data sync` is actually a shorthand command. You could also run these commands one after the other:
```bash ```bash
# Optionally add --parent <parent_dataset_id> if you want to base # Optionally add --parent <parent_dataset_id> if you want to base
# this version on another dataset version, so no duplicate files are uploaded! # this version on another dataset version, so no duplicate files are uploaded!
@ -177,7 +181,7 @@ python utils/loggers/clearml/hpo.py
## 🤯 Remote Execution (advanced) ## 🤯 Remote Execution (advanced)
Running HPO locally is really handy, but what if we want to run our experiments on a remote machine instead? Maybe you have access to a very powerful GPU machine on-site or you have some budget to use cloud GPUs. Running HPO locally is really handy, but what if we want to run our experiments on a remote machine instead? Maybe you have access to a very powerful GPU machine on-site, or you have some budget to use cloud GPUs.
This is where the ClearML Agent comes into play. Check out what the agent can do here: This is where the ClearML Agent comes into play. Check out what the agent can do here:
- [YouTube video](https://youtu.be/MX3BrXnaULs) - [YouTube video](https://youtu.be/MX3BrXnaULs)
@ -186,6 +190,7 @@ This is where the ClearML Agent comes into play. Check out what the agent can do
In short: every experiment tracked by the experiment manager contains enough information to reproduce it on a different machine (installed packages, uncommitted changes etc.). So a ClearML agent does just that: it listens to a queue for incoming tasks and when it finds one, it recreates the environment and runs it while still reporting scalars, plots etc. to the experiment manager. In short: every experiment tracked by the experiment manager contains enough information to reproduce it on a different machine (installed packages, uncommitted changes etc.). So a ClearML agent does just that: it listens to a queue for incoming tasks and when it finds one, it recreates the environment and runs it while still reporting scalars, plots etc. to the experiment manager.
You can turn any machine (a cloud VM, a local GPU machine, your own laptop ... ) into a ClearML agent by simply running: You can turn any machine (a cloud VM, a local GPU machine, your own laptop ... ) into a ClearML agent by simply running:
```bash ```bash
clearml-agent daemon --queue <queues_to_listen_to> [--docker] clearml-agent daemon --queue <queues_to_listen_to> [--docker]
``` ```
@ -194,11 +199,11 @@ clearml-agent daemon --queue <queues_to_listen_to> [--docker]
With our agent running, we can give it some work. Remember from the HPO section that we can clone a task and edit the hyperparameters? We can do that from the interface too! With our agent running, we can give it some work. Remember from the HPO section that we can clone a task and edit the hyperparameters? We can do that from the interface too!
🪄 Clone the experiment by right clicking it 🪄 Clone the experiment by right-clicking it
🎯 Edit the hyperparameters to what you wish them to be 🎯 Edit the hyperparameters to what you wish them to be
⏳ Enqueue the task to any of the queues by right clicking it ⏳ Enqueue the task to any of the queues by right-clicking it
![Enqueue a task from the UI](https://github.com/thepycoder/clearml_screenshots/raw/main/enqueue.gif) ![Enqueue a task from the UI](https://github.com/thepycoder/clearml_screenshots/raw/main/enqueue.gif)
@ -206,7 +211,8 @@ With our agent running, we can give it some work. Remember from the HPO section
Now you can clone a task like we explained above, or simply mark your current script by adding `task.execute_remotely()` and on execution it will be put into a queue, for the agent to start working on! Now you can clone a task like we explained above, or simply mark your current script by adding `task.execute_remotely()` and on execution it will be put into a queue, for the agent to start working on!
To run the YOLOv5 training script remotely, all you have to do is add this line to the training.py script after the clearml logger has been instatiated: To run the YOLOv5 training script remotely, all you have to do is add this line to the training.py script after the clearml logger has been instantiated:
```python ```python
# ... # ...
# Loggers # Loggers
@ -214,16 +220,17 @@ data_dict = None
if RANK in {-1, 0}: if RANK in {-1, 0}:
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance
if loggers.clearml: if loggers.clearml:
loggers.clearml.task.execute_remotely(queue='my_queue') # <------ ADD THIS LINE loggers.clearml.task.execute_remotely(queue="my_queue") # <------ ADD THIS LINE
# Data_dict is either None is user did not choose for ClearML dataset or is filled in by ClearML # Data_dict is either None is user did not choose for ClearML dataset or is filled in by ClearML
data_dict = loggers.clearml.data_dict data_dict = loggers.clearml.data_dict
# ... # ...
``` ```
When running the training script after this change, python will run the script up until that line, after which it will package the code and send it to the queue instead! When running the training script after this change, python will run the script up until that line, after which it will package the code and send it to the queue instead!
### Autoscaling workers ### Autoscaling workers
ClearML comes with autoscalers too! This tool will automatically spin up new remote machines in the cloud of your choice (AWS, GCP, Azure) and turn them into ClearML agents for you whenever there are experiments detected in the queue. Once the tasks are processed, the autoscaler will automatically shut down the remote machines and you stop paying! ClearML comes with autoscalers too! This tool will automatically spin up new remote machines in the cloud of your choice (AWS, GCP, Azure) and turn them into ClearML agents for you whenever there are experiments detected in the queue. Once the tasks are processed, the autoscaler will automatically shut down the remote machines, and you stop paying!
Check out the autoscalers getting started video below. Check out the autoscalers getting started video below.

View File

@ -25,7 +25,7 @@ def construct_dataset(clearml_info_string):
dataset_root_path = Path(dataset.get_local_copy()) dataset_root_path = Path(dataset.get_local_copy())
# We'll search for the yaml file definition in the dataset # We'll search for the yaml file definition in the dataset
yaml_filenames = list(glob.glob(str(dataset_root_path / "*.yaml")) + glob.glob(str(dataset_root_path / "*.yml"))) yaml_filenames = list(glob.glob(str(dataset_root_path / '*.yaml')) + glob.glob(str(dataset_root_path / '*.yml')))
if len(yaml_filenames) > 1: if len(yaml_filenames) > 1:
raise ValueError('More than one yaml file was found in the dataset root, cannot determine which one contains ' raise ValueError('More than one yaml file was found in the dataset root, cannot determine which one contains '
'the dataset definition this way.') 'the dataset definition this way.')
@ -100,7 +100,7 @@ class ClearmlLogger:
self.task.connect(opt, name='Args') self.task.connect(opt, name='Args')
# Make sure the code is easily remotely runnable by setting the docker image to use by the remote agent # Make sure the code is easily remotely runnable by setting the docker image to use by the remote agent
self.task.set_base_docker("ultralytics/yolov5:latest", self.task.set_base_docker('ultralytics/yolov5:latest',
docker_arguments='--ipc=host -e="CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1"', docker_arguments='--ipc=host -e="CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1"',
docker_setup_bash_script='pip install clearml') docker_setup_bash_script='pip install clearml')
@ -150,7 +150,7 @@ class ClearmlLogger:
class_name = class_names[int(class_nr)] class_name = class_names[int(class_nr)]
confidence_percentage = round(float(conf) * 100, 2) confidence_percentage = round(float(conf) * 100, 2)
label = f"{class_name}: {confidence_percentage}%" label = f'{class_name}: {confidence_percentage}%'
if conf > conf_threshold: if conf > conf_threshold:
annotator.rectangle(box.cpu().numpy(), outline=color) annotator.rectangle(box.cpu().numpy(), outline=color)

View File

@ -23,7 +23,7 @@ pip install comet_ml
There are two ways to configure Comet with YOLOv5. There are two ways to configure Comet with YOLOv5.
You can either set your credentials through enviroment variables You can either set your credentials through environment variables
**Environment Variables** **Environment Variables**
@ -49,11 +49,12 @@ project_name=<Your Comet Project Name> # This will default to 'yolov5'
python train.py --img 640 --batch 16 --epochs 5 --data coco128.yaml --weights yolov5s.pt python train.py --img 640 --batch 16 --epochs 5 --data coco128.yaml --weights yolov5s.pt
``` ```
That's it! Comet will automatically log your hyperparameters, command line arguments, training and valiation metrics. You can visualize and analyze your runs in the Comet UI That's it! Comet will automatically log your hyperparameters, command line arguments, training and validation metrics. You can visualize and analyze your runs in the Comet UI
<img width="1920" alt="yolo-ui" src="https://user-images.githubusercontent.com/26833433/202851203-164e94e1-2238-46dd-91f8-de020e9d6b41.png"> <img width="1920" alt="yolo-ui" src="https://user-images.githubusercontent.com/26833433/202851203-164e94e1-2238-46dd-91f8-de020e9d6b41.png">
# Try out an Example! # Try out an Example!
Check out an example of a [completed run here](https://www.comet.com/examples/comet-example-yolov5/a0e29e0e9b984e4a822db2a62d0cb357?experiment-tab=chart&showOutliers=true&smoothing=0&transformY=smoothing&xAxis=step&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github) Check out an example of a [completed run here](https://www.comet.com/examples/comet-example-yolov5/a0e29e0e9b984e4a822db2a62d0cb357?experiment-tab=chart&showOutliers=true&smoothing=0&transformY=smoothing&xAxis=step&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)
Or better yet, try it out yourself in this Colab Notebook Or better yet, try it out yourself in this Colab Notebook
@ -65,6 +66,7 @@ Or better yet, try it out yourself in this Colab Notebook
By default, Comet will log the following items By default, Comet will log the following items
## Metrics ## Metrics
- Box Loss, Object Loss, Classification Loss for the training and validation data - Box Loss, Object Loss, Classification Loss for the training and validation data
- mAP_0.5, mAP_0.5:0.95 metrics for the validation data. - mAP_0.5, mAP_0.5:0.95 metrics for the validation data.
- Precision and Recall for the validation data - Precision and Recall for the validation data
@ -121,7 +123,6 @@ You can control the frequency of logged predictions and the associated images by
Here is an [example project using the Panel](https://www.comet.com/examples/comet-example-yolov5?shareable=YcwMiJaZSXfcEXpGOHDD12vA1&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github) Here is an [example project using the Panel](https://www.comet.com/examples/comet-example-yolov5?shareable=YcwMiJaZSXfcEXpGOHDD12vA1&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)
```shell ```shell
python train.py \ python train.py \
--img 640 \ --img 640 \
@ -192,6 +193,7 @@ If you would like to use a dataset from Comet Artifacts, set the `path` variable
# contents of artifact.yaml file # contents of artifact.yaml file
path: "comet://<workspace name>/<artifact name>:<artifact version or alias>" path: "comet://<workspace name>/<artifact name>:<artifact version or alias>"
``` ```
Then pass this file to your training script in the following way Then pass this file to your training script in the following way
```shell ```shell
@ -221,7 +223,7 @@ python train.py \
## Hyperparameter Search with the Comet Optimizer ## Hyperparameter Search with the Comet Optimizer
YOLOv5 is also integrated with Comet's Optimizer, making is simple to visualie hyperparameter sweeps in the Comet UI. YOLOv5 is also integrated with Comet's Optimizer, making is simple to visualize hyperparameter sweeps in the Comet UI.
### Configuring an Optimizer Sweep ### Configuring an Optimizer Sweep

View File

@ -17,7 +17,7 @@ try:
# Project Configuration # Project Configuration
config = comet_ml.config.get_config() config = comet_ml.config.get_config()
COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5") COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5')
except (ModuleNotFoundError, ImportError): except (ModuleNotFoundError, ImportError):
comet_ml = None comet_ml = None
COMET_PROJECT_NAME = None COMET_PROJECT_NAME = None
@ -31,32 +31,32 @@ from utils.dataloaders import img2label_paths
from utils.general import check_dataset, scale_boxes, xywh2xyxy from utils.general import check_dataset, scale_boxes, xywh2xyxy
from utils.metrics import box_iou from utils.metrics import box_iou
COMET_PREFIX = "comet://" COMET_PREFIX = 'comet://'
COMET_MODE = os.getenv("COMET_MODE", "online") COMET_MODE = os.getenv('COMET_MODE', 'online')
# Model Saving Settings # Model Saving Settings
COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5") COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5')
# Dataset Artifact Settings # Dataset Artifact Settings
COMET_UPLOAD_DATASET = os.getenv("COMET_UPLOAD_DATASET", "false").lower() == "true" COMET_UPLOAD_DATASET = os.getenv('COMET_UPLOAD_DATASET', 'false').lower() == 'true'
# Evaluation Settings # Evaluation Settings
COMET_LOG_CONFUSION_MATRIX = os.getenv("COMET_LOG_CONFUSION_MATRIX", "true").lower() == "true" COMET_LOG_CONFUSION_MATRIX = os.getenv('COMET_LOG_CONFUSION_MATRIX', 'true').lower() == 'true'
COMET_LOG_PREDICTIONS = os.getenv("COMET_LOG_PREDICTIONS", "true").lower() == "true" COMET_LOG_PREDICTIONS = os.getenv('COMET_LOG_PREDICTIONS', 'true').lower() == 'true'
COMET_MAX_IMAGE_UPLOADS = int(os.getenv("COMET_MAX_IMAGE_UPLOADS", 100)) COMET_MAX_IMAGE_UPLOADS = int(os.getenv('COMET_MAX_IMAGE_UPLOADS', 100))
# Confusion Matrix Settings # Confusion Matrix Settings
CONF_THRES = float(os.getenv("CONF_THRES", 0.001)) CONF_THRES = float(os.getenv('CONF_THRES', 0.001))
IOU_THRES = float(os.getenv("IOU_THRES", 0.6)) IOU_THRES = float(os.getenv('IOU_THRES', 0.6))
# Batch Logging Settings # Batch Logging Settings
COMET_LOG_BATCH_METRICS = os.getenv("COMET_LOG_BATCH_METRICS", "false").lower() == "true" COMET_LOG_BATCH_METRICS = os.getenv('COMET_LOG_BATCH_METRICS', 'false').lower() == 'true'
COMET_BATCH_LOGGING_INTERVAL = os.getenv("COMET_BATCH_LOGGING_INTERVAL", 1) COMET_BATCH_LOGGING_INTERVAL = os.getenv('COMET_BATCH_LOGGING_INTERVAL', 1)
COMET_PREDICTION_LOGGING_INTERVAL = os.getenv("COMET_PREDICTION_LOGGING_INTERVAL", 1) COMET_PREDICTION_LOGGING_INTERVAL = os.getenv('COMET_PREDICTION_LOGGING_INTERVAL', 1)
COMET_LOG_PER_CLASS_METRICS = os.getenv("COMET_LOG_PER_CLASS_METRICS", "false").lower() == "true" COMET_LOG_PER_CLASS_METRICS = os.getenv('COMET_LOG_PER_CLASS_METRICS', 'false').lower() == 'true'
RANK = int(os.getenv("RANK", -1)) RANK = int(os.getenv('RANK', -1))
to_pil = T.ToPILImage() to_pil = T.ToPILImage()
@ -66,7 +66,7 @@ class CometLogger:
with Comet with Comet
""" """
def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwargs) -> None: def __init__(self, opt, hyp, run_id=None, job_type='Training', **experiment_kwargs) -> None:
self.job_type = job_type self.job_type = job_type
self.opt = opt self.opt = opt
self.hyp = hyp self.hyp = hyp
@ -87,52 +87,52 @@ class CometLogger:
# Default parameters to pass to Experiment objects # Default parameters to pass to Experiment objects
self.default_experiment_kwargs = { self.default_experiment_kwargs = {
"log_code": False, 'log_code': False,
"log_env_gpu": True, 'log_env_gpu': True,
"log_env_cpu": True, 'log_env_cpu': True,
"project_name": COMET_PROJECT_NAME,} 'project_name': COMET_PROJECT_NAME,}
self.default_experiment_kwargs.update(experiment_kwargs) self.default_experiment_kwargs.update(experiment_kwargs)
self.experiment = self._get_experiment(self.comet_mode, run_id) self.experiment = self._get_experiment(self.comet_mode, run_id)
self.data_dict = self.check_dataset(self.opt.data) self.data_dict = self.check_dataset(self.opt.data)
self.class_names = self.data_dict["names"] self.class_names = self.data_dict['names']
self.num_classes = self.data_dict["nc"] self.num_classes = self.data_dict['nc']
self.logged_images_count = 0 self.logged_images_count = 0
self.max_images = COMET_MAX_IMAGE_UPLOADS self.max_images = COMET_MAX_IMAGE_UPLOADS
if run_id is None: if run_id is None:
self.experiment.log_other("Created from", "YOLOv5") self.experiment.log_other('Created from', 'YOLOv5')
if not isinstance(self.experiment, comet_ml.OfflineExperiment): if not isinstance(self.experiment, comet_ml.OfflineExperiment):
workspace, project_name, experiment_id = self.experiment.url.split("/")[-3:] workspace, project_name, experiment_id = self.experiment.url.split('/')[-3:]
self.experiment.log_other( self.experiment.log_other(
"Run Path", 'Run Path',
f"{workspace}/{project_name}/{experiment_id}", f'{workspace}/{project_name}/{experiment_id}',
) )
self.log_parameters(vars(opt)) self.log_parameters(vars(opt))
self.log_parameters(self.opt.hyp) self.log_parameters(self.opt.hyp)
self.log_asset_data( self.log_asset_data(
self.opt.hyp, self.opt.hyp,
name="hyperparameters.json", name='hyperparameters.json',
metadata={"type": "hyp-config-file"}, metadata={'type': 'hyp-config-file'},
) )
self.log_asset( self.log_asset(
f"{self.opt.save_dir}/opt.yaml", f'{self.opt.save_dir}/opt.yaml',
metadata={"type": "opt-config-file"}, metadata={'type': 'opt-config-file'},
) )
self.comet_log_confusion_matrix = COMET_LOG_CONFUSION_MATRIX self.comet_log_confusion_matrix = COMET_LOG_CONFUSION_MATRIX
if hasattr(self.opt, "conf_thres"): if hasattr(self.opt, 'conf_thres'):
self.conf_thres = self.opt.conf_thres self.conf_thres = self.opt.conf_thres
else: else:
self.conf_thres = CONF_THRES self.conf_thres = CONF_THRES
if hasattr(self.opt, "iou_thres"): if hasattr(self.opt, 'iou_thres'):
self.iou_thres = self.opt.iou_thres self.iou_thres = self.opt.iou_thres
else: else:
self.iou_thres = IOU_THRES self.iou_thres = IOU_THRES
self.log_parameters({"val_iou_threshold": self.iou_thres, "val_conf_threshold": self.conf_thres}) self.log_parameters({'val_iou_threshold': self.iou_thres, 'val_conf_threshold': self.conf_thres})
self.comet_log_predictions = COMET_LOG_PREDICTIONS self.comet_log_predictions = COMET_LOG_PREDICTIONS
if self.opt.bbox_interval == -1: if self.opt.bbox_interval == -1:
@ -147,22 +147,22 @@ class CometLogger:
self.comet_log_per_class_metrics = COMET_LOG_PER_CLASS_METRICS self.comet_log_per_class_metrics = COMET_LOG_PER_CLASS_METRICS
self.experiment.log_others({ self.experiment.log_others({
"comet_mode": COMET_MODE, 'comet_mode': COMET_MODE,
"comet_max_image_uploads": COMET_MAX_IMAGE_UPLOADS, 'comet_max_image_uploads': COMET_MAX_IMAGE_UPLOADS,
"comet_log_per_class_metrics": COMET_LOG_PER_CLASS_METRICS, 'comet_log_per_class_metrics': COMET_LOG_PER_CLASS_METRICS,
"comet_log_batch_metrics": COMET_LOG_BATCH_METRICS, 'comet_log_batch_metrics': COMET_LOG_BATCH_METRICS,
"comet_log_confusion_matrix": COMET_LOG_CONFUSION_MATRIX, 'comet_log_confusion_matrix': COMET_LOG_CONFUSION_MATRIX,
"comet_model_name": COMET_MODEL_NAME,}) 'comet_model_name': COMET_MODEL_NAME,})
# Check if running the Experiment with the Comet Optimizer # Check if running the Experiment with the Comet Optimizer
if hasattr(self.opt, "comet_optimizer_id"): if hasattr(self.opt, 'comet_optimizer_id'):
self.experiment.log_other("optimizer_id", self.opt.comet_optimizer_id) self.experiment.log_other('optimizer_id', self.opt.comet_optimizer_id)
self.experiment.log_other("optimizer_objective", self.opt.comet_optimizer_objective) self.experiment.log_other('optimizer_objective', self.opt.comet_optimizer_objective)
self.experiment.log_other("optimizer_metric", self.opt.comet_optimizer_metric) self.experiment.log_other('optimizer_metric', self.opt.comet_optimizer_metric)
self.experiment.log_other("optimizer_parameters", json.dumps(self.hyp)) self.experiment.log_other('optimizer_parameters', json.dumps(self.hyp))
def _get_experiment(self, mode, experiment_id=None): def _get_experiment(self, mode, experiment_id=None):
if mode == "offline": if mode == 'offline':
if experiment_id is not None: if experiment_id is not None:
return comet_ml.ExistingOfflineExperiment( return comet_ml.ExistingOfflineExperiment(
previous_experiment=experiment_id, previous_experiment=experiment_id,
@ -182,11 +182,11 @@ class CometLogger:
return comet_ml.Experiment(**self.default_experiment_kwargs) return comet_ml.Experiment(**self.default_experiment_kwargs)
except ValueError: except ValueError:
logger.warning("COMET WARNING: " logger.warning('COMET WARNING: '
"Comet credentials have not been set. " 'Comet credentials have not been set. '
"Comet will default to offline logging. " 'Comet will default to offline logging. '
"Please set your credentials to enable online logging.") 'Please set your credentials to enable online logging.')
return self._get_experiment("offline", experiment_id) return self._get_experiment('offline', experiment_id)
return return
@ -210,12 +210,12 @@ class CometLogger:
return return
model_metadata = { model_metadata = {
"fitness_score": fitness_score[-1], 'fitness_score': fitness_score[-1],
"epochs_trained": epoch + 1, 'epochs_trained': epoch + 1,
"save_period": opt.save_period, 'save_period': opt.save_period,
"total_epochs": opt.epochs,} 'total_epochs': opt.epochs,}
model_files = glob.glob(f"{path}/*.pt") model_files = glob.glob(f'{path}/*.pt')
for model_path in model_files: for model_path in model_files:
name = Path(model_path).name name = Path(model_path).name
@ -232,12 +232,12 @@ class CometLogger:
data_config = yaml.safe_load(f) data_config = yaml.safe_load(f)
if data_config['path'].startswith(COMET_PREFIX): if data_config['path'].startswith(COMET_PREFIX):
path = data_config['path'].replace(COMET_PREFIX, "") path = data_config['path'].replace(COMET_PREFIX, '')
data_dict = self.download_dataset_artifact(path) data_dict = self.download_dataset_artifact(path)
return data_dict return data_dict
self.log_asset(self.opt.data, metadata={"type": "data-config-file"}) self.log_asset(self.opt.data, metadata={'type': 'data-config-file'})
return check_dataset(data_file) return check_dataset(data_file)
@ -253,8 +253,8 @@ class CometLogger:
filtered_detections = detections[mask] filtered_detections = detections[mask]
filtered_labels = labelsn[mask] filtered_labels = labelsn[mask]
image_id = path.split("/")[-1].split(".")[0] image_id = path.split('/')[-1].split('.')[0]
image_name = f"{image_id}_curr_epoch_{self.experiment.curr_epoch}" image_name = f'{image_id}_curr_epoch_{self.experiment.curr_epoch}'
if image_name not in self.logged_image_names: if image_name not in self.logged_image_names:
native_scale_image = PIL.Image.open(path) native_scale_image = PIL.Image.open(path)
self.log_image(native_scale_image, name=image_name) self.log_image(native_scale_image, name=image_name)
@ -263,22 +263,22 @@ class CometLogger:
metadata = [] metadata = []
for cls, *xyxy in filtered_labels.tolist(): for cls, *xyxy in filtered_labels.tolist():
metadata.append({ metadata.append({
"label": f"{self.class_names[int(cls)]}-gt", 'label': f'{self.class_names[int(cls)]}-gt',
"score": 100, 'score': 100,
"box": { 'box': {
"x": xyxy[0], 'x': xyxy[0],
"y": xyxy[1], 'y': xyxy[1],
"x2": xyxy[2], 'x2': xyxy[2],
"y2": xyxy[3]},}) 'y2': xyxy[3]},})
for *xyxy, conf, cls in filtered_detections.tolist(): for *xyxy, conf, cls in filtered_detections.tolist():
metadata.append({ metadata.append({
"label": f"{self.class_names[int(cls)]}", 'label': f'{self.class_names[int(cls)]}',
"score": conf * 100, 'score': conf * 100,
"box": { 'box': {
"x": xyxy[0], 'x': xyxy[0],
"y": xyxy[1], 'y': xyxy[1],
"x2": xyxy[2], 'x2': xyxy[2],
"y2": xyxy[3]},}) 'y2': xyxy[3]},})
self.metadata_dict[image_name] = metadata self.metadata_dict[image_name] = metadata
self.logged_images_count += 1 self.logged_images_count += 1
@ -305,35 +305,35 @@ class CometLogger:
return predn, labelsn return predn, labelsn
def add_assets_to_artifact(self, artifact, path, asset_path, split): def add_assets_to_artifact(self, artifact, path, asset_path, split):
img_paths = sorted(glob.glob(f"{asset_path}/*")) img_paths = sorted(glob.glob(f'{asset_path}/*'))
label_paths = img2label_paths(img_paths) label_paths = img2label_paths(img_paths)
for image_file, label_file in zip(img_paths, label_paths): for image_file, label_file in zip(img_paths, label_paths):
image_logical_path, label_logical_path = map(lambda x: os.path.relpath(x, path), [image_file, label_file]) image_logical_path, label_logical_path = map(lambda x: os.path.relpath(x, path), [image_file, label_file])
try: try:
artifact.add(image_file, logical_path=image_logical_path, metadata={"split": split}) artifact.add(image_file, logical_path=image_logical_path, metadata={'split': split})
artifact.add(label_file, logical_path=label_logical_path, metadata={"split": split}) artifact.add(label_file, logical_path=label_logical_path, metadata={'split': split})
except ValueError as e: except ValueError as e:
logger.error('COMET ERROR: Error adding file to Artifact. Skipping file.') logger.error('COMET ERROR: Error adding file to Artifact. Skipping file.')
logger.error(f"COMET ERROR: {e}") logger.error(f'COMET ERROR: {e}')
continue continue
return artifact return artifact
def upload_dataset_artifact(self): def upload_dataset_artifact(self):
dataset_name = self.data_dict.get("dataset_name", "yolov5-dataset") dataset_name = self.data_dict.get('dataset_name', 'yolov5-dataset')
path = str((ROOT / Path(self.data_dict["path"])).resolve()) path = str((ROOT / Path(self.data_dict['path'])).resolve())
metadata = self.data_dict.copy() metadata = self.data_dict.copy()
for key in ["train", "val", "test"]: for key in ['train', 'val', 'test']:
split_path = metadata.get(key) split_path = metadata.get(key)
if split_path is not None: if split_path is not None:
metadata[key] = split_path.replace(path, "") metadata[key] = split_path.replace(path, '')
artifact = comet_ml.Artifact(name=dataset_name, artifact_type="dataset", metadata=metadata) artifact = comet_ml.Artifact(name=dataset_name, artifact_type='dataset', metadata=metadata)
for key in metadata.keys(): for key in metadata.keys():
if key in ["train", "val", "test"]: if key in ['train', 'val', 'test']:
if isinstance(self.upload_dataset, str) and (key != self.upload_dataset): if isinstance(self.upload_dataset, str) and (key != self.upload_dataset):
continue continue
@ -352,13 +352,13 @@ class CometLogger:
metadata = logged_artifact.metadata metadata = logged_artifact.metadata
data_dict = metadata.copy() data_dict = metadata.copy()
data_dict["path"] = artifact_save_dir data_dict['path'] = artifact_save_dir
metadata_names = metadata.get("names") metadata_names = metadata.get('names')
if type(metadata_names) == dict: if type(metadata_names) == dict:
data_dict["names"] = {int(k): v for k, v in metadata.get("names").items()} data_dict['names'] = {int(k): v for k, v in metadata.get('names').items()}
elif type(metadata_names) == list: elif type(metadata_names) == list:
data_dict["names"] = {int(k): v for k, v in zip(range(len(metadata_names)), metadata_names)} data_dict['names'] = {int(k): v for k, v in zip(range(len(metadata_names)), metadata_names)}
else: else:
raise "Invalid 'names' field in dataset yaml file. Please use a list or dictionary" raise "Invalid 'names' field in dataset yaml file. Please use a list or dictionary"
@ -366,13 +366,13 @@ class CometLogger:
return data_dict return data_dict
def update_data_paths(self, data_dict): def update_data_paths(self, data_dict):
path = data_dict.get("path", "") path = data_dict.get('path', '')
for split in ["train", "val", "test"]: for split in ['train', 'val', 'test']:
if data_dict.get(split): if data_dict.get(split):
split_path = data_dict.get(split) split_path = data_dict.get(split)
data_dict[split] = (f"{path}/{split_path}" if isinstance(split, str) else [ data_dict[split] = (f'{path}/{split_path}' if isinstance(split, str) else [
f"{path}/{x}" for x in split_path]) f'{path}/{x}' for x in split_path])
return data_dict return data_dict
@ -413,11 +413,11 @@ class CometLogger:
def on_train_end(self, files, save_dir, last, best, epoch, results): def on_train_end(self, files, save_dir, last, best, epoch, results):
if self.comet_log_predictions: if self.comet_log_predictions:
curr_epoch = self.experiment.curr_epoch curr_epoch = self.experiment.curr_epoch
self.experiment.log_asset_data(self.metadata_dict, "image-metadata.json", epoch=curr_epoch) self.experiment.log_asset_data(self.metadata_dict, 'image-metadata.json', epoch=curr_epoch)
for f in files: for f in files:
self.log_asset(f, metadata={"epoch": epoch}) self.log_asset(f, metadata={'epoch': epoch})
self.log_asset(f"{save_dir}/results.csv", metadata={"epoch": epoch}) self.log_asset(f'{save_dir}/results.csv', metadata={'epoch': epoch})
if not self.opt.evolve: if not self.opt.evolve:
model_path = str(best if best.exists() else last) model_path = str(best if best.exists() else last)
@ -481,7 +481,7 @@ class CometLogger:
if self.comet_log_confusion_matrix: if self.comet_log_confusion_matrix:
epoch = self.experiment.curr_epoch epoch = self.experiment.curr_epoch
class_names = list(self.class_names.values()) class_names = list(self.class_names.values())
class_names.append("background") class_names.append('background')
num_classes = len(class_names) num_classes = len(class_names)
self.experiment.log_confusion_matrix( self.experiment.log_confusion_matrix(
@ -491,7 +491,7 @@ class CometLogger:
epoch=epoch, epoch=epoch,
column_label='Actual Category', column_label='Actual Category',
row_label='Predicted Category', row_label='Predicted Category',
file_name=f"confusion-matrix-epoch-{epoch}.json", file_name=f'confusion-matrix-epoch-{epoch}.json',
) )
def on_fit_epoch_end(self, result, epoch): def on_fit_epoch_end(self, result, epoch):

View File

@ -11,28 +11,28 @@ import yaml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
COMET_PREFIX = "comet://" COMET_PREFIX = 'comet://'
COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5") COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5')
COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv("COMET_DEFAULT_CHECKPOINT_FILENAME", "last.pt") COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv('COMET_DEFAULT_CHECKPOINT_FILENAME', 'last.pt')
def download_model_checkpoint(opt, experiment): def download_model_checkpoint(opt, experiment):
model_dir = f"{opt.project}/{experiment.name}" model_dir = f'{opt.project}/{experiment.name}'
os.makedirs(model_dir, exist_ok=True) os.makedirs(model_dir, exist_ok=True)
model_name = COMET_MODEL_NAME model_name = COMET_MODEL_NAME
model_asset_list = experiment.get_model_asset_list(model_name) model_asset_list = experiment.get_model_asset_list(model_name)
if len(model_asset_list) == 0: if len(model_asset_list) == 0:
logger.error(f"COMET ERROR: No checkpoints found for model name : {model_name}") logger.error(f'COMET ERROR: No checkpoints found for model name : {model_name}')
return return
model_asset_list = sorted( model_asset_list = sorted(
model_asset_list, model_asset_list,
key=lambda x: x["step"], key=lambda x: x['step'],
reverse=True, reverse=True,
) )
logged_checkpoint_map = {asset["fileName"]: asset["assetId"] for asset in model_asset_list} logged_checkpoint_map = {asset['fileName']: asset['assetId'] for asset in model_asset_list}
resource_url = urlparse(opt.weights) resource_url = urlparse(opt.weights)
checkpoint_filename = resource_url.query checkpoint_filename = resource_url.query
@ -44,22 +44,22 @@ def download_model_checkpoint(opt, experiment):
checkpoint_filename = COMET_DEFAULT_CHECKPOINT_FILENAME checkpoint_filename = COMET_DEFAULT_CHECKPOINT_FILENAME
if asset_id is None: if asset_id is None:
logger.error(f"COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment") logger.error(f'COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment')
return return
try: try:
logger.info(f"COMET INFO: Downloading checkpoint {checkpoint_filename}") logger.info(f'COMET INFO: Downloading checkpoint {checkpoint_filename}')
asset_filename = checkpoint_filename asset_filename = checkpoint_filename
model_binary = experiment.get_asset(asset_id, return_type="binary", stream=False) model_binary = experiment.get_asset(asset_id, return_type='binary', stream=False)
model_download_path = f"{model_dir}/{asset_filename}" model_download_path = f'{model_dir}/{asset_filename}'
with open(model_download_path, "wb") as f: with open(model_download_path, 'wb') as f:
f.write(model_binary) f.write(model_binary)
opt.weights = model_download_path opt.weights = model_download_path
except Exception as e: except Exception as e:
logger.warning("COMET WARNING: Unable to download checkpoint from Comet") logger.warning('COMET WARNING: Unable to download checkpoint from Comet')
logger.exception(e) logger.exception(e)
@ -75,9 +75,9 @@ def set_opt_parameters(opt, experiment):
resume_string = opt.resume resume_string = opt.resume
for asset in asset_list: for asset in asset_list:
if asset["fileName"] == "opt.yaml": if asset['fileName'] == 'opt.yaml':
asset_id = asset["assetId"] asset_id = asset['assetId']
asset_binary = experiment.get_asset(asset_id, return_type="binary", stream=False) asset_binary = experiment.get_asset(asset_id, return_type='binary', stream=False)
opt_dict = yaml.safe_load(asset_binary) opt_dict = yaml.safe_load(asset_binary)
for key, value in opt_dict.items(): for key, value in opt_dict.items():
setattr(opt, key, value) setattr(opt, key, value)
@ -85,11 +85,11 @@ def set_opt_parameters(opt, experiment):
# Save hyperparameters to YAML file # Save hyperparameters to YAML file
# Necessary to pass checks in training script # Necessary to pass checks in training script
save_dir = f"{opt.project}/{experiment.name}" save_dir = f'{opt.project}/{experiment.name}'
os.makedirs(save_dir, exist_ok=True) os.makedirs(save_dir, exist_ok=True)
hyp_yaml_path = f"{save_dir}/hyp.yaml" hyp_yaml_path = f'{save_dir}/hyp.yaml'
with open(hyp_yaml_path, "w") as f: with open(hyp_yaml_path, 'w') as f:
yaml.dump(opt.hyp, f) yaml.dump(opt.hyp, f)
opt.hyp = hyp_yaml_path opt.hyp = hyp_yaml_path
@ -113,7 +113,7 @@ def check_comet_weights(opt):
if opt.weights.startswith(COMET_PREFIX): if opt.weights.startswith(COMET_PREFIX):
api = comet_ml.API() api = comet_ml.API()
resource = urlparse(opt.weights) resource = urlparse(opt.weights)
experiment_path = f"{resource.netloc}{resource.path}" experiment_path = f'{resource.netloc}{resource.path}'
experiment = api.get(experiment_path) experiment = api.get(experiment_path)
download_model_checkpoint(opt, experiment) download_model_checkpoint(opt, experiment)
return True return True
@ -140,7 +140,7 @@ def check_comet_resume(opt):
if opt.resume.startswith(COMET_PREFIX): if opt.resume.startswith(COMET_PREFIX):
api = comet_ml.API() api = comet_ml.API()
resource = urlparse(opt.resume) resource = urlparse(opt.resume)
experiment_path = f"{resource.netloc}{resource.path}" experiment_path = f'{resource.netloc}{resource.path}'
experiment = api.get(experiment_path) experiment = api.get(experiment_path)
set_opt_parameters(opt, experiment) set_opt_parameters(opt, experiment)
download_model_checkpoint(opt, experiment) download_model_checkpoint(opt, experiment)

View File

@ -21,7 +21,7 @@ from utils.torch_utils import select_device
# Project Configuration # Project Configuration
config = comet_ml.config.get_config() config = comet_ml.config.get_config()
COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5") COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5')
def get_args(known=False): def get_args(known=False):
@ -68,30 +68,30 @@ def get_args(known=False):
parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use') parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')
# Comet Arguments # Comet Arguments
parser.add_argument("--comet_optimizer_config", type=str, help="Comet: Path to a Comet Optimizer Config File.") parser.add_argument('--comet_optimizer_config', type=str, help='Comet: Path to a Comet Optimizer Config File.')
parser.add_argument("--comet_optimizer_id", type=str, help="Comet: ID of the Comet Optimizer sweep.") parser.add_argument('--comet_optimizer_id', type=str, help='Comet: ID of the Comet Optimizer sweep.')
parser.add_argument("--comet_optimizer_objective", type=str, help="Comet: Set to 'minimize' or 'maximize'.") parser.add_argument('--comet_optimizer_objective', type=str, help="Comet: Set to 'minimize' or 'maximize'.")
parser.add_argument("--comet_optimizer_metric", type=str, help="Comet: Metric to Optimize.") parser.add_argument('--comet_optimizer_metric', type=str, help='Comet: Metric to Optimize.')
parser.add_argument("--comet_optimizer_workers", parser.add_argument('--comet_optimizer_workers',
type=int, type=int,
default=1, default=1,
help="Comet: Number of Parallel Workers to use with the Comet Optimizer.") help='Comet: Number of Parallel Workers to use with the Comet Optimizer.')
return parser.parse_known_args()[0] if known else parser.parse_args() return parser.parse_known_args()[0] if known else parser.parse_args()
def run(parameters, opt): def run(parameters, opt):
hyp_dict = {k: v for k, v in parameters.items() if k not in ["epochs", "batch_size"]} hyp_dict = {k: v for k, v in parameters.items() if k not in ['epochs', 'batch_size']}
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve)) opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve))
opt.batch_size = parameters.get("batch_size") opt.batch_size = parameters.get('batch_size')
opt.epochs = parameters.get("epochs") opt.epochs = parameters.get('epochs')
device = select_device(opt.device, batch_size=opt.batch_size) device = select_device(opt.device, batch_size=opt.batch_size)
train(hyp_dict, opt, device, callbacks=Callbacks()) train(hyp_dict, opt, device, callbacks=Callbacks())
if __name__ == "__main__": if __name__ == '__main__':
opt = get_args(known=True) opt = get_args(known=True)
opt.weights = str(opt.weights) opt.weights = str(opt.weights)
@ -99,7 +99,7 @@ if __name__ == "__main__":
opt.data = str(opt.data) opt.data = str(opt.data)
opt.project = str(opt.project) opt.project = str(opt.project)
optimizer_id = os.getenv("COMET_OPTIMIZER_ID") optimizer_id = os.getenv('COMET_OPTIMIZER_ID')
if optimizer_id is None: if optimizer_id is None:
with open(opt.comet_optimizer_config) as f: with open(opt.comet_optimizer_config) as f:
optimizer_config = json.load(f) optimizer_config = json.load(f)
@ -110,9 +110,9 @@ if __name__ == "__main__":
opt.comet_optimizer_id = optimizer.id opt.comet_optimizer_id = optimizer.id
status = optimizer.status() status = optimizer.status()
opt.comet_optimizer_objective = status["spec"]["objective"] opt.comet_optimizer_objective = status['spec']['objective']
opt.comet_optimizer_metric = status["spec"]["metric"] opt.comet_optimizer_metric = status['spec']['metric']
logger.info("COMET INFO: Starting Hyperparameter Sweep") logger.info('COMET INFO: Starting Hyperparameter Sweep')
for parameter in optimizer.get_parameters(): for parameter in optimizer.get_parameters():
run(parameter["parameters"], opt) run(parameter['parameters'], opt)

View File

@ -1,110 +1,32 @@
"""Utilities and tools for tracking runs with Weights & Biases.""" # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# WARNING ⚠️ wandb is deprecated and will be removed in future release.
# See supported integrations at https://github.com/ultralytics/yolov5#integrations
import logging import logging
import os import os
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Dict
import yaml from utils.general import LOGGER, colorstr
from tqdm import tqdm
FILE = Path(__file__).resolve() FILE = Path(__file__).resolve()
ROOT = FILE.parents[3] # YOLOv5 root directory ROOT = FILE.parents[3] # YOLOv5 root directory
if str(ROOT) not in sys.path: if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH sys.path.append(str(ROOT)) # add ROOT to PATH
RANK = int(os.getenv('RANK', -1))
from utils.dataloaders import LoadImagesAndLabels, img2label_paths DEPRECATION_WARNING = f"{colorstr('wandb')}: WARNING ⚠️ wandb is deprecated and will be removed in a future release. " \
from utils.general import LOGGER, check_dataset, check_file f'See supported integrations at https://github.com/ultralytics/yolov5#integrations.'
try: try:
import wandb import wandb
assert hasattr(wandb, '__version__') # verify package import not local dir assert hasattr(wandb, '__version__') # verify package import not local dir
LOGGER.warning(DEPRECATION_WARNING)
except (ImportError, AssertionError): except (ImportError, AssertionError):
wandb = None wandb = None
RANK = int(os.getenv('RANK', -1))
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX):
return from_string[len(prefix):]
def check_wandb_config_file(data_config_file):
wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path
if Path(wandb_config).is_file():
return wandb_config
return data_config_file
def check_wandb_dataset(data_file):
is_trainset_wandb_artifact = False
is_valset_wandb_artifact = False
if isinstance(data_file, dict):
# In that case another dataset manager has already processed it and we don't have to
return data_file
if check_file(data_file) and data_file.endswith('.yaml'):
with open(data_file, errors='ignore') as f:
data_dict = yaml.safe_load(f)
is_trainset_wandb_artifact = isinstance(data_dict['train'],
str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX)
is_valset_wandb_artifact = isinstance(data_dict['val'],
str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX)
if is_trainset_wandb_artifact or is_valset_wandb_artifact:
return data_dict
else:
return check_dataset(data_file)
def get_run_info(run_path):
run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX))
run_id = run_path.stem
project = run_path.parent.stem
entity = run_path.parent.parent.stem
model_artifact_name = 'run_' + run_id + '_model'
return entity, project, run_id, model_artifact_name
def check_wandb_resume(opt):
process_wandb_config_ddp_mode(opt) if RANK not in [-1, 0] else None
if isinstance(opt.resume, str):
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
if RANK not in [-1, 0]: # For resuming DDP runs
entity, project, run_id, model_artifact_name = get_run_info(opt.resume)
api = wandb.Api()
artifact = api.artifact(entity + '/' + project + '/' + model_artifact_name + ':latest')
modeldir = artifact.download()
opt.weights = str(Path(modeldir) / "last.pt")
return True
return None
def process_wandb_config_ddp_mode(opt):
with open(check_file(opt.data), errors='ignore') as f:
data_dict = yaml.safe_load(f) # data dict
train_dir, val_dir = None, None
if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX):
api = wandb.Api()
train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias)
train_dir = train_artifact.download()
train_path = Path(train_dir) / 'data/images/'
data_dict['train'] = str(train_path)
if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX):
api = wandb.Api()
val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias)
val_dir = val_artifact.download()
val_path = Path(val_dir) / 'data/images/'
data_dict['val'] = str(val_path)
if train_dir or val_dir:
ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml')
with open(ddp_data_path, 'w') as f:
yaml.safe_dump(data_dict, f)
opt.data = ddp_data_path
class WandbLogger(): class WandbLogger():
"""Log training runs, datasets, models, and predictions to Weights & Biases. """Log training runs, datasets, models, and predictions to Weights & Biases.
@ -132,91 +54,33 @@ class WandbLogger():
job_type (str) -- To set the job_type for this run job_type (str) -- To set the job_type for this run
""" """
# Temporary-fix
if opt.upload_dataset:
opt.upload_dataset = False
# LOGGER.info("Uploading Dataset functionality is not being supported temporarily due to a bug.")
# Pre-training routine -- # Pre-training routine --
self.job_type = job_type self.job_type = job_type
self.wandb, self.wandb_run = wandb, None if not wandb else wandb.run self.wandb, self.wandb_run = wandb, wandb.run if wandb else None
self.val_artifact, self.train_artifact = None, None self.val_artifact, self.train_artifact = None, None
self.train_artifact_path, self.val_artifact_path = None, None self.train_artifact_path, self.val_artifact_path = None, None
self.result_artifact = None self.result_artifact = None
self.val_table, self.result_table = None, None self.val_table, self.result_table = None, None
self.bbox_media_panel_images = []
self.val_table_path_map = None
self.max_imgs_to_log = 16 self.max_imgs_to_log = 16
self.wandb_artifact_data_dict = None
self.data_dict = None self.data_dict = None
# It's more elegant to stick to 1 wandb.init call, if self.wandb:
# but useful config data is overwritten in the WandbLogger's wandb.init call
if isinstance(opt.resume, str): # checks resume from artifact
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
entity, project, run_id, model_artifact_name = get_run_info(opt.resume)
model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name
assert wandb, 'install wandb to resume wandb runs'
# Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config
self.wandb_run = wandb.init(id=run_id,
project=project,
entity=entity,
resume='allow',
allow_val_change=True)
opt.resume = model_artifact_name
elif self.wandb:
self.wandb_run = wandb.init(config=opt, self.wandb_run = wandb.init(config=opt,
resume="allow", resume='allow',
project='YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem, project='YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem,
entity=opt.entity, entity=opt.entity,
name=opt.name if opt.name != 'exp' else None, name=opt.name if opt.name != 'exp' else None,
job_type=job_type, job_type=job_type,
id=run_id, id=run_id,
allow_val_change=True) if not wandb.run else wandb.run allow_val_change=True) if not wandb.run else wandb.run
if self.wandb_run: if self.wandb_run:
if self.job_type == 'Training': if self.job_type == 'Training':
if opt.upload_dataset:
if not opt.resume:
self.wandb_artifact_data_dict = self.check_and_upload_dataset(opt)
if isinstance(opt.data, dict): if isinstance(opt.data, dict):
# This means another dataset manager has already processed the dataset info (e.g. ClearML) # This means another dataset manager has already processed the dataset info (e.g. ClearML)
# and they will have stored the already processed dict in opt.data # and they will have stored the already processed dict in opt.data
self.data_dict = opt.data self.data_dict = opt.data
elif opt.resume:
# resume from artifact
if isinstance(opt.resume, str) and opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
self.data_dict = dict(self.wandb_run.config.data_dict)
else: # local resume
self.data_dict = check_wandb_dataset(opt.data)
else:
self.data_dict = check_wandb_dataset(opt.data)
self.wandb_artifact_data_dict = self.wandb_artifact_data_dict or self.data_dict
# write data_dict to config. useful for resuming from artifacts. Do this only when not resuming.
self.wandb_run.config.update({'data_dict': self.wandb_artifact_data_dict}, allow_val_change=True)
self.setup_training(opt) self.setup_training(opt)
if self.job_type == 'Dataset Creation':
self.wandb_run.config.update({"upload_dataset": True})
self.data_dict = self.check_and_upload_dataset(opt)
def check_and_upload_dataset(self, opt):
"""
Check if the dataset format is compatible and upload it as W&B artifact
arguments:
opt (namespace)-- Commandline arguments for current run
returns:
Updated dataset info dictionary where local dataset paths are replaced by WAND_ARFACT_PREFIX links.
"""
assert wandb, 'Install wandb to upload dataset'
config_path = self.log_dataset_artifact(opt.data, opt.single_cls,
'YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem)
with open(config_path, errors='ignore') as f:
wandb_data_dict = yaml.safe_load(f)
return wandb_data_dict
def setup_training(self, opt): def setup_training(self, opt):
""" """
Setup the necessary processes for training YOLO models: Setup the necessary processes for training YOLO models:
@ -231,81 +95,18 @@ class WandbLogger():
self.log_dict, self.current_epoch = {}, 0 self.log_dict, self.current_epoch = {}, 0
self.bbox_interval = opt.bbox_interval self.bbox_interval = opt.bbox_interval
if isinstance(opt.resume, str): if isinstance(opt.resume, str):
modeldir, _ = self.download_model_artifact(opt) model_dir, _ = self.download_model_artifact(opt)
if modeldir: if model_dir:
self.weights = Path(modeldir) / "last.pt" self.weights = Path(model_dir) / 'last.pt'
config = self.wandb_run.config config = self.wandb_run.config
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp, opt.imgsz = str( opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp, opt.imgsz = str(
self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs,\ self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \
config.hyp, config.imgsz config.hyp, config.imgsz
data_dict = self.data_dict
if self.val_artifact is None: # If --upload_dataset is set, use the existing artifact, don't download
self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(
data_dict.get('train'), opt.artifact_alias)
self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(
data_dict.get('val'), opt.artifact_alias)
if self.train_artifact_path is not None:
train_path = Path(self.train_artifact_path) / 'data/images/'
data_dict['train'] = str(train_path)
if self.val_artifact_path is not None:
val_path = Path(self.val_artifact_path) / 'data/images/'
data_dict['val'] = str(val_path)
if self.val_artifact is not None:
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
columns = ["epoch", "id", "ground truth", "prediction"]
columns.extend(self.data_dict['names'])
self.result_table = wandb.Table(columns)
self.val_table = self.val_artifact.get("val")
if self.val_table_path_map is None:
self.map_val_table_path()
if opt.bbox_interval == -1: if opt.bbox_interval == -1:
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1 self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
if opt.evolve or opt.noplots: if opt.evolve or opt.noplots:
self.bbox_interval = opt.bbox_interval = opt.epochs + 1 # disable bbox_interval self.bbox_interval = opt.bbox_interval = opt.epochs + 1 # disable bbox_interval
train_from_artifact = self.train_artifact_path is not None and self.val_artifact_path is not None
# Update the the data_dict to point to local artifacts dir
if train_from_artifact:
self.data_dict = data_dict
def download_dataset_artifact(self, path, alias):
"""
download the model checkpoint artifact if the path starts with WANDB_ARTIFACT_PREFIX
arguments:
path -- path of the dataset to be used for training
alias (str)-- alias of the artifact to be download/used for training
returns:
(str, wandb.Artifact) -- path of the downladed dataset and it's corresponding artifact object if dataset
is found otherwise returns (None, None)
"""
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
dataset_artifact = wandb.use_artifact(artifact_path.as_posix().replace("\\", "/"))
assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
datadir = dataset_artifact.download()
return datadir, dataset_artifact
return None, None
def download_model_artifact(self, opt):
"""
download the model checkpoint artifact if the resume path starts with WANDB_ARTIFACT_PREFIX
arguments:
opt (namespace) -- Commandline arguments for this run
"""
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest")
assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist'
modeldir = model_artifact.download()
# epochs_trained = model_artifact.metadata.get('epochs_trained')
total_epochs = model_artifact.metadata.get('total_epochs')
is_finished = total_epochs is None
assert not is_finished, 'training is finished, can only resume incomplete runs.'
return modeldir, model_artifact
return None, None
def log_model(self, path, opt, epoch, fitness_score, best_model=False): def log_model(self, path, opt, epoch, fitness_score, best_model=False):
""" """
@ -330,192 +131,10 @@ class WandbLogger():
model_artifact.add_file(str(path / 'last.pt'), name='last.pt') model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
wandb.log_artifact(model_artifact, wandb.log_artifact(model_artifact,
aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else '']) aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
LOGGER.info(f"Saving model artifact on epoch {epoch + 1}") LOGGER.info(f'Saving model artifact on epoch {epoch + 1}')
def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False):
"""
Log the dataset as W&B artifact and return the new data file with W&B links
arguments:
data_file (str) -- the .yaml file with information about the dataset like - path, classes etc.
single_class (boolean) -- train multi-class data as single-class
project (str) -- project name. Used to construct the artifact path
overwrite_config (boolean) -- overwrites the data.yaml file if set to true otherwise creates a new
file with _wandb postfix. Eg -> data_wandb.yaml
returns:
the new .yaml file with artifact links. it can be used to start training directly from artifacts
"""
upload_dataset = self.wandb_run.config.upload_dataset
log_val_only = isinstance(upload_dataset, str) and upload_dataset == 'val'
self.data_dict = check_dataset(data_file) # parse and check
data = dict(self.data_dict)
nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names'])
names = {k: v for k, v in enumerate(names)} # to index dictionary
# log train set
if not log_val_only:
self.train_artifact = self.create_dataset_table(LoadImagesAndLabels(data['train'], rect=True, batch_size=1),
names,
name='train') if data.get('train') else None
if data.get('train'):
data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train')
self.val_artifact = self.create_dataset_table(
LoadImagesAndLabels(data['val'], rect=True, batch_size=1), names, name='val') if data.get('val') else None
if data.get('val'):
data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val')
path = Path(data_file)
# create a _wandb.yaml file with artifacts links if both train and test set are logged
if not log_val_only:
path = (path.stem if overwrite_config else path.stem + '_wandb') + '.yaml' # updated data.yaml path
path = ROOT / 'data' / path
data.pop('download', None)
data.pop('path', None)
with open(path, 'w') as f:
yaml.safe_dump(data, f)
LOGGER.info(f"Created dataset config file {path}")
if self.job_type == 'Training': # builds correct artifact pipeline graph
if not log_val_only:
self.wandb_run.log_artifact(
self.train_artifact) # calling use_artifact downloads the dataset. NOT NEEDED!
self.wandb_run.use_artifact(self.val_artifact)
self.val_artifact.wait()
self.val_table = self.val_artifact.get('val')
self.map_val_table_path()
else:
self.wandb_run.log_artifact(self.train_artifact)
self.wandb_run.log_artifact(self.val_artifact)
return path
def map_val_table_path(self):
"""
Map the validation dataset Table like name of file -> it's id in the W&B Table.
Useful for - referencing artifacts for evaluation.
"""
self.val_table_path_map = {}
LOGGER.info("Mapping dataset")
for i, data in enumerate(tqdm(self.val_table.data)):
self.val_table_path_map[data[3]] = data[0]
def create_dataset_table(self, dataset: LoadImagesAndLabels, class_to_id: Dict[int, str], name: str = 'dataset'):
"""
Create and return W&B artifact containing W&B Table of the dataset.
arguments:
dataset -- instance of LoadImagesAndLabels class used to iterate over the data to build Table
class_to_id -- hash map that maps class ids to labels
name -- name of the artifact
returns:
dataset artifact to be logged or used
"""
# TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
artifact = wandb.Artifact(name=name, type="dataset")
img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None
img_files = tqdm(dataset.im_files) if not img_files else img_files
for img_file in img_files:
if Path(img_file).is_dir():
artifact.add_dir(img_file, name='data/images')
labels_path = 'labels'.join(dataset.path.rsplit('images', 1))
artifact.add_dir(labels_path, name='data/labels')
else:
artifact.add_file(img_file, name='data/images/' + Path(img_file).name)
label_file = Path(img2label_paths([img_file])[0])
artifact.add_file(str(label_file), name='data/labels/' +
label_file.name) if label_file.exists() else None
table = wandb.Table(columns=["id", "train_image", "Classes", "name"])
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()])
for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)):
box_data, img_classes = [], {}
for cls, *xywh in labels[:, 1:].tolist():
cls = int(cls)
box_data.append({
"position": {
"middle": [xywh[0], xywh[1]],
"width": xywh[2],
"height": xywh[3]},
"class_id": cls,
"box_caption": "%s" % (class_to_id[cls])})
img_classes[cls] = class_to_id[cls]
boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space
table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), list(img_classes.values()),
Path(paths).name)
artifact.add(table, name)
return artifact
def log_training_progress(self, predn, path, names):
"""
Build evaluation Table. Uses reference from validation dataset table.
arguments:
predn (list): list of predictions in the native space in the format - [xmin, ymin, xmax, ymax, confidence, class]
path (str): local path of the current evaluation image
names (dict(int, str)): hash map that maps class ids to labels
"""
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
box_data = []
avg_conf_per_class = [0] * len(self.data_dict['names'])
pred_class_count = {}
for *xyxy, conf, cls in predn.tolist():
if conf >= 0.25:
cls = int(cls)
box_data.append({
"position": {
"minX": xyxy[0],
"minY": xyxy[1],
"maxX": xyxy[2],
"maxY": xyxy[3]},
"class_id": cls,
"box_caption": f"{names[cls]} {conf:.3f}",
"scores": {
"class_score": conf},
"domain": "pixel"})
avg_conf_per_class[cls] += conf
if cls in pred_class_count:
pred_class_count[cls] += 1
else:
pred_class_count[cls] = 1
for pred_class in pred_class_count.keys():
avg_conf_per_class[pred_class] = avg_conf_per_class[pred_class] / pred_class_count[pred_class]
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
id = self.val_table_path_map[Path(path).name]
self.result_table.add_data(self.current_epoch, id, self.val_table.data[id][1],
wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set),
*avg_conf_per_class)
def val_one_image(self, pred, predn, path, names, im): def val_one_image(self, pred, predn, path, names, im):
""" pass
Log validation data for one image. updates the result Table if validation dataset is uploaded and log bbox media panel
arguments:
pred (list): list of scaled predictions in the format - [xmin, ymin, xmax, ymax, confidence, class]
predn (list): list of predictions in the native space - [xmin, ymin, xmax, ymax, confidence, class]
path (str): local path of the current evaluation image
"""
if self.val_table and self.result_table: # Log Table if Val dataset is uploaded as artifact
self.log_training_progress(predn, path, names)
if len(self.bbox_media_panel_images) < self.max_imgs_to_log and self.current_epoch > 0:
if self.current_epoch % self.bbox_interval == 0:
box_data = [{
"position": {
"minX": xyxy[0],
"minY": xyxy[1],
"maxX": xyxy[2],
"maxY": xyxy[3]},
"class_id": int(cls),
"box_caption": f"{names[int(cls)]} {conf:.3f}",
"scores": {
"class_score": conf},
"domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
self.bbox_media_panel_images.append(wandb.Image(im, boxes=boxes, caption=path.name))
def log(self, log_dict): def log(self, log_dict):
""" """
@ -528,7 +147,7 @@ class WandbLogger():
for key, value in log_dict.items(): for key, value in log_dict.items():
self.log_dict[key] = value self.log_dict[key] = value
def end_epoch(self, best_result=False): def end_epoch(self):
""" """
commit the log_dict, model artifacts and Tables to W&B and flush the log_dict. commit the log_dict, model artifacts and Tables to W&B and flush the log_dict.
@ -537,31 +156,15 @@ class WandbLogger():
""" """
if self.wandb_run: if self.wandb_run:
with all_logging_disabled(): with all_logging_disabled():
if self.bbox_media_panel_images:
self.log_dict["BoundingBoxDebugger"] = self.bbox_media_panel_images
try: try:
wandb.log(self.log_dict) wandb.log(self.log_dict)
except BaseException as e: except BaseException as e:
LOGGER.info( LOGGER.info(
f"An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}" f'An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}'
) )
self.wandb_run.finish() self.wandb_run.finish()
self.wandb_run = None self.wandb_run = None
self.log_dict = {} self.log_dict = {}
self.bbox_media_panel_images = []
if self.result_artifact:
self.result_artifact.add(self.result_table, 'result')
wandb.log_artifact(self.result_artifact,
aliases=[
'latest', 'last', 'epoch ' + str(self.current_epoch),
('best' if best_result else '')])
wandb.log({"evaluation": self.result_table})
columns = ["epoch", "id", "ground truth", "prediction"]
columns.extend(self.data_dict['names'])
self.result_table = wandb.Table(columns)
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
def finish_run(self): def finish_run(self):
""" """
@ -572,6 +175,7 @@ class WandbLogger():
with all_logging_disabled(): with all_logging_disabled():
wandb.log(self.log_dict) wandb.log(self.log_dict)
wandb.run.finish() wandb.run.finish()
LOGGER.warning(DEPRECATION_WARNING)
@contextmanager @contextmanager

View File

@ -28,7 +28,7 @@ def smooth(y, f=0.05):
return np.convolve(yp, np.ones(nf) / nf, mode='valid') # y-smoothed return np.convolve(yp, np.ones(nf) / nf, mode='valid') # y-smoothed
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16, prefix=""): def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16, prefix=''):
""" Compute the average precision, given the recall and precision curves. """ Compute the average precision, given the recall and precision curves.
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
# Arguments # Arguments
@ -194,21 +194,21 @@ class ConfusionMatrix:
nc, nn = self.nc, len(names) # number of classes, names nc, nn = self.nc, len(names) # number of classes, names
sn.set(font_scale=1.0 if nc < 50 else 0.8) # for label size sn.set(font_scale=1.0 if nc < 50 else 0.8) # for label size
labels = (0 < nn < 99) and (nn == nc) # apply names to ticklabels labels = (0 < nn < 99) and (nn == nc) # apply names to ticklabels
ticklabels = (names + ['background']) if labels else "auto" ticklabels = (names + ['background']) if labels else 'auto'
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress empty matrix RuntimeWarning: All-NaN slice encountered warnings.simplefilter('ignore') # suppress empty matrix RuntimeWarning: All-NaN slice encountered
sn.heatmap(array, sn.heatmap(array,
ax=ax, ax=ax,
annot=nc < 30, annot=nc < 30,
annot_kws={ annot_kws={
"size": 8}, 'size': 8},
cmap='Blues', cmap='Blues',
fmt='.2f', fmt='.2f',
square=True, square=True,
vmin=0.0, vmin=0.0,
xticklabels=ticklabels, xticklabels=ticklabels,
yticklabels=ticklabels).set_facecolor((1, 1, 1)) yticklabels=ticklabels).set_facecolor((1, 1, 1))
ax.set_ylabel('True') ax.set_xlabel('True')
ax.set_ylabel('Predicted') ax.set_ylabel('Predicted')
ax.set_title('Confusion Matrix') ax.set_title('Confusion Matrix')
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250) fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
@ -331,7 +331,7 @@ def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()):
ax.set_ylabel('Precision') ax.set_ylabel('Precision')
ax.set_xlim(0, 1) ax.set_xlim(0, 1)
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left") ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left')
ax.set_title('Precision-Recall Curve') ax.set_title('Precision-Recall Curve')
fig.savefig(save_dir, dpi=250) fig.savefig(save_dir, dpi=250)
plt.close(fig) plt.close(fig)
@ -354,7 +354,7 @@ def plot_mc_curve(px, py, save_dir=Path('mc_curve.png'), names=(), xlabel='Confi
ax.set_ylabel(ylabel) ax.set_ylabel(ylabel)
ax.set_xlim(0, 1) ax.set_xlim(0, 1)
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left") ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left')
ax.set_title(f'{ylabel}-Confidence Curve') ax.set_title(f'{ylabel}-Confidence Curve')
fig.savefig(save_dir, dpi=250) fig.savefig(save_dir, dpi=250)
plt.close(fig) plt.close(fig)

View File

@ -88,7 +88,8 @@ class Annotator:
if self.pil or not is_ascii(label): if self.pil or not is_ascii(label):
self.draw.rectangle(box, width=self.lw, outline=color) # box self.draw.rectangle(box, width=self.lw, outline=color) # box
if label: if label:
w, h = self.font.getsize(label) # text width, height w, h = self.font.getsize(label) # text width, height (WARNING: deprecated) in 9.2.0
# _, _, w, h = self.font.getbbox(label) # text width, height (New)
outside = box[1] - h >= 0 # label fits outside box outside = box[1] - h >= 0 # label fits outside box
self.draw.rectangle( self.draw.rectangle(
(box[0], box[1] - h if outside else box[1], box[0] + w + 1, (box[0], box[1] - h if outside else box[1], box[0] + w + 1,
@ -449,7 +450,7 @@ def imshow_cls(im, labels=None, pred=None, names=None, nmax=25, verbose=False, f
plt.savefig(f, dpi=300, bbox_inches='tight') plt.savefig(f, dpi=300, bbox_inches='tight')
plt.close() plt.close()
if verbose: if verbose:
LOGGER.info(f"Saving {f}") LOGGER.info(f'Saving {f}')
if labels is not None: if labels is not None:
LOGGER.info('True: ' + ' '.join(f'{names[i]:3s}' for i in labels[:nmax])) LOGGER.info('True: ' + ' '.join(f'{names[i]:3s}' for i in labels[:nmax]))
if pred is not None: if pred is not None:

View File

@ -95,7 +95,7 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
stride=32, stride=32,
pad=0, pad=0,
min_items=0, min_items=0,
prefix="", prefix='',
downsample_ratio=1, downsample_ratio=1,
overlap=False, overlap=False,
): ):
@ -116,7 +116,7 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
shapes = None shapes = None
# MixUp augmentation # MixUp augmentation
if random.random() < hyp["mixup"]: if random.random() < hyp['mixup']:
img, labels, segments = mixup(img, labels, segments, *self.load_mosaic(random.randint(0, self.n - 1))) img, labels, segments = mixup(img, labels, segments, *self.load_mosaic(random.randint(0, self.n - 1)))
else: else:
@ -147,11 +147,11 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
img, labels, segments = random_perspective(img, img, labels, segments = random_perspective(img,
labels, labels,
segments=segments, segments=segments,
degrees=hyp["degrees"], degrees=hyp['degrees'],
translate=hyp["translate"], translate=hyp['translate'],
scale=hyp["scale"], scale=hyp['scale'],
shear=hyp["shear"], shear=hyp['shear'],
perspective=hyp["perspective"]) perspective=hyp['perspective'])
nl = len(labels) # number of labels nl = len(labels) # number of labels
if nl: if nl:
@ -177,17 +177,17 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
nl = len(labels) # update after albumentations nl = len(labels) # update after albumentations
# HSV color-space # HSV color-space
augment_hsv(img, hgain=hyp["hsv_h"], sgain=hyp["hsv_s"], vgain=hyp["hsv_v"]) augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v'])
# Flip up-down # Flip up-down
if random.random() < hyp["flipud"]: if random.random() < hyp['flipud']:
img = np.flipud(img) img = np.flipud(img)
if nl: if nl:
labels[:, 2] = 1 - labels[:, 2] labels[:, 2] = 1 - labels[:, 2]
masks = torch.flip(masks, dims=[1]) masks = torch.flip(masks, dims=[1])
# Flip left-right # Flip left-right
if random.random() < hyp["fliplr"]: if random.random() < hyp['fliplr']:
img = np.fliplr(img) img = np.fliplr(img)
if nl: if nl:
labels[:, 1] = 1 - labels[:, 1] labels[:, 1] = 1 - labels[:, 1]
@ -251,15 +251,15 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
# img4, labels4 = replicate(img4, labels4) # replicate # img4, labels4 = replicate(img4, labels4) # replicate
# Augment # Augment
img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp["copy_paste"]) img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste'])
img4, labels4, segments4 = random_perspective(img4, img4, labels4, segments4 = random_perspective(img4,
labels4, labels4,
segments4, segments4,
degrees=self.hyp["degrees"], degrees=self.hyp['degrees'],
translate=self.hyp["translate"], translate=self.hyp['translate'],
scale=self.hyp["scale"], scale=self.hyp['scale'],
shear=self.hyp["shear"], shear=self.hyp['shear'],
perspective=self.hyp["perspective"], perspective=self.hyp['perspective'],
border=self.mosaic_border) # border to remove border=self.mosaic_border) # border to remove
return img4, labels4, segments4 return img4, labels4, segments4

View File

@ -83,7 +83,7 @@ class ComputeLoss:
# Mask regression # Mask regression
if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample
masks = F.interpolate(masks[None], (mask_h, mask_w), mode="nearest")[0] masks = F.interpolate(masks[None], (mask_h, mask_w), mode='nearest')[0]
marea = xywhn[i][:, 2:].prod(1) # mask width, height normalized marea = xywhn[i][:, 2:].prod(1) # mask width, height normalized
mxyxy = xywh2xyxy(xywhn[i] * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device)) mxyxy = xywh2xyxy(xywhn[i] * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device))
for bi in b.unique(): for bi in b.unique():
@ -101,10 +101,10 @@ class ComputeLoss:
if self.autobalance: if self.autobalance:
self.balance = [x / self.balance[self.ssi] for x in self.balance] self.balance = [x / self.balance[self.ssi] for x in self.balance]
lbox *= self.hyp["box"] lbox *= self.hyp['box']
lobj *= self.hyp["obj"] lobj *= self.hyp['obj']
lcls *= self.hyp["cls"] lcls *= self.hyp['cls']
lseg *= self.hyp["box"] / bs lseg *= self.hyp['box'] / bs
loss = lbox + lobj + lcls + lseg loss = lbox + lobj + lcls + lseg
return loss * bs, torch.cat((lbox, lseg, lobj, lcls)).detach() return loss * bs, torch.cat((lbox, lseg, lobj, lcls)).detach()
@ -112,7 +112,7 @@ class ComputeLoss:
def single_mask_loss(self, gt_mask, pred, proto, xyxy, area): def single_mask_loss(self, gt_mask, pred, proto, xyxy, area):
# Mask loss for one image # Mask loss for one image
pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n,32) @ (32,80,80) -> (n,80,80) pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n,32) @ (32,80,80) -> (n,80,80)
loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction="none") loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction='none')
return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean() return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean()
def build_targets(self, p, targets): def build_targets(self, p, targets):

View File

@ -21,7 +21,7 @@ def ap_per_class_box_and_mask(
pred_cls, pred_cls,
target_cls, target_cls,
plot=False, plot=False,
save_dir=".", save_dir='.',
names=(), names=(),
): ):
""" """
@ -37,7 +37,7 @@ def ap_per_class_box_and_mask(
plot=plot, plot=plot,
save_dir=save_dir, save_dir=save_dir,
names=names, names=names,
prefix="Box")[2:] prefix='Box')[2:]
results_masks = ap_per_class(tp_m, results_masks = ap_per_class(tp_m,
conf, conf,
pred_cls, pred_cls,
@ -45,21 +45,21 @@ def ap_per_class_box_and_mask(
plot=plot, plot=plot,
save_dir=save_dir, save_dir=save_dir,
names=names, names=names,
prefix="Mask")[2:] prefix='Mask')[2:]
results = { results = {
"boxes": { 'boxes': {
"p": results_boxes[0], 'p': results_boxes[0],
"r": results_boxes[1], 'r': results_boxes[1],
"ap": results_boxes[3], 'ap': results_boxes[3],
"f1": results_boxes[2], 'f1': results_boxes[2],
"ap_class": results_boxes[4]}, 'ap_class': results_boxes[4]},
"masks": { 'masks': {
"p": results_masks[0], 'p': results_masks[0],
"r": results_masks[1], 'r': results_masks[1],
"ap": results_masks[3], 'ap': results_masks[3],
"f1": results_masks[2], 'f1': results_masks[2],
"ap_class": results_masks[4]}} 'ap_class': results_masks[4]}}
return results return results
@ -159,8 +159,8 @@ class Metrics:
Args: Args:
results: Dict{'boxes': Dict{}, 'masks': Dict{}} results: Dict{'boxes': Dict{}, 'masks': Dict{}}
""" """
self.metric_box.update(list(results["boxes"].values())) self.metric_box.update(list(results['boxes'].values()))
self.metric_mask.update(list(results["masks"].values())) self.metric_mask.update(list(results['masks'].values()))
def mean_results(self): def mean_results(self):
return self.metric_box.mean_results() + self.metric_mask.mean_results() return self.metric_box.mean_results() + self.metric_mask.mean_results()
@ -178,33 +178,33 @@ class Metrics:
KEYS = [ KEYS = [
"train/box_loss", 'train/box_loss',
"train/seg_loss", # train loss 'train/seg_loss', # train loss
"train/obj_loss", 'train/obj_loss',
"train/cls_loss", 'train/cls_loss',
"metrics/precision(B)", 'metrics/precision(B)',
"metrics/recall(B)", 'metrics/recall(B)',
"metrics/mAP_0.5(B)", 'metrics/mAP_0.5(B)',
"metrics/mAP_0.5:0.95(B)", # metrics 'metrics/mAP_0.5:0.95(B)', # metrics
"metrics/precision(M)", 'metrics/precision(M)',
"metrics/recall(M)", 'metrics/recall(M)',
"metrics/mAP_0.5(M)", 'metrics/mAP_0.5(M)',
"metrics/mAP_0.5:0.95(M)", # metrics 'metrics/mAP_0.5:0.95(M)', # metrics
"val/box_loss", 'val/box_loss',
"val/seg_loss", # val loss 'val/seg_loss', # val loss
"val/obj_loss", 'val/obj_loss',
"val/cls_loss", 'val/cls_loss',
"x/lr0", 'x/lr0',
"x/lr1", 'x/lr1',
"x/lr2",] 'x/lr2',]
BEST_KEYS = [ BEST_KEYS = [
"best/epoch", 'best/epoch',
"best/precision(B)", 'best/precision(B)',
"best/recall(B)", 'best/recall(B)',
"best/mAP_0.5(B)", 'best/mAP_0.5(B)',
"best/mAP_0.5:0.95(B)", 'best/mAP_0.5:0.95(B)',
"best/precision(M)", 'best/precision(M)',
"best/recall(M)", 'best/recall(M)',
"best/mAP_0.5(M)", 'best/mAP_0.5(M)',
"best/mAP_0.5:0.95(M)",] 'best/mAP_0.5:0.95(M)',]

View File

@ -108,13 +108,13 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
annotator.im.save(fname) # save annotator.im.save(fname) # save
def plot_results_with_masks(file="path/to/results.csv", dir="", best=True): def plot_results_with_masks(file='path/to/results.csv', dir='', best=True):
# Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv')
save_dir = Path(file).parent if file else Path(dir) save_dir = Path(file).parent if file else Path(dir)
fig, ax = plt.subplots(2, 8, figsize=(18, 6), tight_layout=True) fig, ax = plt.subplots(2, 8, figsize=(18, 6), tight_layout=True)
ax = ax.ravel() ax = ax.ravel()
files = list(save_dir.glob("results*.csv")) files = list(save_dir.glob('results*.csv'))
assert len(files), f"No results.csv files found in {save_dir.resolve()}, nothing to plot." assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.'
for f in files: for f in files:
try: try:
data = pd.read_csv(f) data = pd.read_csv(f)
@ -125,19 +125,19 @@ def plot_results_with_masks(file="path/to/results.csv", dir="", best=True):
for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]): for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]):
y = data.values[:, j] y = data.values[:, j]
# y[y == 0] = np.nan # don't show zero values # y[y == 0] = np.nan # don't show zero values
ax[i].plot(x, y, marker=".", label=f.stem, linewidth=2, markersize=2) ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=2)
if best: if best:
# best # best
ax[i].scatter(index, y[index], color="r", label=f"best:{index}", marker="*", linewidth=3) ax[i].scatter(index, y[index], color='r', label=f'best:{index}', marker='*', linewidth=3)
ax[i].set_title(s[j] + f"\n{round(y[index], 5)}") ax[i].set_title(s[j] + f'\n{round(y[index], 5)}')
else: else:
# last # last
ax[i].scatter(x[-1], y[-1], color="r", label="last", marker="*", linewidth=3) ax[i].scatter(x[-1], y[-1], color='r', label='last', marker='*', linewidth=3)
ax[i].set_title(s[j] + f"\n{round(y[-1], 5)}") ax[i].set_title(s[j] + f'\n{round(y[-1], 5)}')
# if j in [8, 9, 10]: # share train and val loss y axes # if j in [8, 9, 10]: # share train and val loss y axes
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
except Exception as e: except Exception as e:
print(f"Warning: Plotting error for {f}: {e}") print(f'Warning: Plotting error for {f}: {e}')
ax[1].legend() ax[1].legend()
fig.savefig(save_dir / "results.png", dpi=200) fig.savefig(save_dir / 'results.png', dpi=200)
plt.close() plt.close()

View File

@ -291,7 +291,7 @@ def model_info(model, verbose=False, imgsz=640):
fs = '' fs = ''
name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model' name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model'
LOGGER.info(f"{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}") LOGGER.info(f'{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}')
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
@ -342,7 +342,7 @@ def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):
optimizer.add_param_group({'params': g[0], 'weight_decay': decay}) # add g0 with weight_decay optimizer.add_param_group({'params': g[0], 'weight_decay': decay}) # add g0 with weight_decay
optimizer.add_param_group({'params': g[1], 'weight_decay': 0.0}) # add g1 (BatchNorm2d weights) optimizer.add_param_group({'params': g[1], 'weight_decay': 0.0}) # add g1 (BatchNorm2d weights)
LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups " LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups "
f"{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias") f'{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias')
return optimizer return optimizer

View File

@ -21,7 +21,7 @@ class TritonRemoteModel:
""" """
parsed_url = urlparse(url) parsed_url = urlparse(url)
if parsed_url.scheme == "grpc": if parsed_url.scheme == 'grpc':
from tritonclient.grpc import InferenceServerClient, InferInput from tritonclient.grpc import InferenceServerClient, InferInput
self.client = InferenceServerClient(parsed_url.netloc) # Triton GRPC client self.client = InferenceServerClient(parsed_url.netloc) # Triton GRPC client
@ -31,7 +31,7 @@ class TritonRemoteModel:
def create_input_placeholders() -> typing.List[InferInput]: def create_input_placeholders() -> typing.List[InferInput]:
return [ return [
InferInput(i['name'], [int(s) for s in i["shape"]], i['datatype']) for i in self.metadata['inputs']] InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']]
else: else:
from tritonclient.http import InferenceServerClient, InferInput from tritonclient.http import InferenceServerClient, InferInput
@ -43,14 +43,14 @@ class TritonRemoteModel:
def create_input_placeholders() -> typing.List[InferInput]: def create_input_placeholders() -> typing.List[InferInput]:
return [ return [
InferInput(i['name'], [int(s) for s in i["shape"]], i['datatype']) for i in self.metadata['inputs']] InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']]
self._create_input_placeholders_fn = create_input_placeholders self._create_input_placeholders_fn = create_input_placeholders
@property @property
def runtime(self): def runtime(self):
"""Returns the model runtime""" """Returns the model runtime"""
return self.metadata.get("backend", self.metadata.get("platform")) return self.metadata.get('backend', self.metadata.get('platform'))
def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[torch.Tensor, ...]]: def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[torch.Tensor, ...]]:
""" Invokes the model. Parameters can be provided via args or kwargs. """ Invokes the model. Parameters can be provided via args or kwargs.
@ -68,14 +68,14 @@ class TritonRemoteModel:
def _create_inputs(self, *args, **kwargs): def _create_inputs(self, *args, **kwargs):
args_len, kwargs_len = len(args), len(kwargs) args_len, kwargs_len = len(args), len(kwargs)
if not args_len and not kwargs_len: if not args_len and not kwargs_len:
raise RuntimeError("No inputs provided.") raise RuntimeError('No inputs provided.')
if args_len and kwargs_len: if args_len and kwargs_len:
raise RuntimeError("Cannot specify args and kwargs at the same time") raise RuntimeError('Cannot specify args and kwargs at the same time')
placeholders = self._create_input_placeholders_fn() placeholders = self._create_input_placeholders_fn()
if args_len: if args_len:
if args_len != len(placeholders): if args_len != len(placeholders):
raise RuntimeError(f"Expected {len(placeholders)} inputs, got {args_len}.") raise RuntimeError(f'Expected {len(placeholders)} inputs, got {args_len}.')
for input, value in zip(placeholders, args): for input, value in zip(placeholders, args):
input.set_data_from_numpy(value.cpu().numpy()) input.set_data_from_numpy(value.cpu().numpy())
else: else: