mirror of
https://github.com/ultralytics/yolov5.git
synced 2025-06-03 14:49:29 +08:00
Merge master
This commit is contained in:
commit
cdd161a539
5
.github/workflows/ci-testing.yml
vendored
5
.github/workflows/ci-testing.yml
vendored
@ -51,12 +51,15 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -qr requirements.txt -f https://download.pytorch.org/whl/cpu/torch_stable.html
|
||||
pip install -q onnx tensorflow-cpu keras==2.6.0 # for export
|
||||
pip install -q onnx tensorflow-cpu keras==2.6.0 # wandb # extras
|
||||
python --version
|
||||
pip --version
|
||||
pip list
|
||||
shell: bash
|
||||
|
||||
# - name: W&B login
|
||||
# run: wandb login 345011b3fb26dc8337fd9b20e53857c1d403f2aa
|
||||
|
||||
- name: Download data
|
||||
run: |
|
||||
# curl -L -o tmp.zip https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip
|
||||
|
@ -1,7 +1,7 @@
|
||||
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
|
||||
|
||||
# Start FROM Nvidia PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch
|
||||
FROM nvcr.io/nvidia/pytorch:21.05-py3
|
||||
FROM nvcr.io/nvidia/pytorch:21.10-py3
|
||||
|
||||
# Install linux packages
|
||||
RUN apt update && apt install -y zip htop screen libgl1-mesa-glx
|
||||
@ -11,8 +11,8 @@ COPY requirements.txt .
|
||||
RUN python -m pip install --upgrade pip
|
||||
RUN pip uninstall -y nvidia-tensorboard nvidia-tensorboard-plugin-dlprof
|
||||
RUN pip install --no-cache -r requirements.txt coremltools onnx gsutil notebook wandb>=0.12.2
|
||||
RUN pip install --no-cache -U torch torchvision numpy
|
||||
# RUN pip install --no-cache torch==1.9.1+cu111 torchvision==0.10.1+cu111 -f https://download.pytorch.org/whl/torch_stable.html
|
||||
RUN pip install --no-cache -U torch torchvision numpy Pillow
|
||||
# RUN pip install --no-cache torch==1.10.0+cu113 torchvision==0.11.1+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html
|
||||
|
||||
# Create working directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
@ -109,11 +109,11 @@ the [latest YOLOv5 release](https://github.com/ultralytics/yolov5/releases) and
|
||||
|
||||
```bash
|
||||
$ python detect.py --source 0 # webcam
|
||||
file.jpg # image
|
||||
file.mp4 # video
|
||||
img.jpg # image
|
||||
vid.mp4 # video
|
||||
path/ # directory
|
||||
path/*.jpg # glob
|
||||
'https://youtu.be/NUsoVlDFqZg' # YouTube
|
||||
'https://youtu.be/Zgi9g1ksQHc' # YouTube
|
||||
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
|
||||
```
|
||||
|
||||
|
@ -27,4 +27,4 @@ names: ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 't
|
||||
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip
|
||||
download: https://ultralytics.com/assets/coco128.zip
|
||||
|
145
detect.py
145
detect.py
@ -3,17 +3,21 @@
|
||||
Run inference on images, videos, directories, streams, etc.
|
||||
|
||||
Usage:
|
||||
$ python path/to/detect.py --source path/to/img.jpg --weights yolov5s.pt --img 640
|
||||
$ python path/to/detect.py --weights yolov5s.pt --source 0 # webcam
|
||||
img.jpg # image
|
||||
vid.mp4 # video
|
||||
path/ # directory
|
||||
path/*.jpg # glob
|
||||
'https://youtu.be/Zgi9g1ksQHc' # YouTube
|
||||
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.backends.cudnn as cudnn
|
||||
|
||||
@ -23,13 +27,12 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT)) # add ROOT to PATH
|
||||
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
|
||||
|
||||
from models.experimental import attempt_load
|
||||
from models.common import DetectMultiBackend
|
||||
from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams
|
||||
from utils.general import (LOGGER, apply_classifier, check_file, check_img_size, check_imshow, check_requirements,
|
||||
check_suffix, colorstr, increment_path, non_max_suppression, print_args, save_one_box,
|
||||
scale_coords, strip_optimizer, xyxy2xywh)
|
||||
from utils.plots import Annotator, colors
|
||||
from utils.torch_utils import load_classifier, select_device, time_sync
|
||||
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
|
||||
increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh)
|
||||
from utils.plots import Annotator, colors, save_one_box
|
||||
from utils.torch_utils import select_device, time_sync
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
@ -71,120 +74,45 @@ def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s)
|
||||
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
|
||||
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
|
||||
|
||||
# Initialize
|
||||
device = select_device(device)
|
||||
half &= device.type != 'cpu' # half precision only supported on CUDA
|
||||
|
||||
# Load model
|
||||
w = str(weights[0] if isinstance(weights, list) else weights)
|
||||
classify, suffix, suffixes = False, Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '']
|
||||
check_suffix(w, suffixes) # check weights have acceptable suffix
|
||||
pt, onnx, tflite, pb, saved_model = (suffix == x for x in suffixes) # backend booleans
|
||||
stride, names = 64, [f'class{i}' for i in range(1000)] # assign defaults
|
||||
if pt:
|
||||
model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device)
|
||||
stride = int(model.stride.max()) # model stride
|
||||
names = model.module.names if hasattr(model, 'module') else model.names # get class names
|
||||
if half:
|
||||
model.half() # to FP16
|
||||
if classify: # second-stage classifier
|
||||
modelc = load_classifier(name='resnet50', n=2) # initialize
|
||||
modelc.load_state_dict(torch.load('resnet50.pt', map_location=device)['model']).to(device).eval()
|
||||
elif onnx:
|
||||
if dnn:
|
||||
check_requirements(('opencv-python>=4.5.4',))
|
||||
net = cv2.dnn.readNetFromONNX(w)
|
||||
else:
|
||||
check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime'))
|
||||
import onnxruntime
|
||||
session = onnxruntime.InferenceSession(w, None)
|
||||
else: # TensorFlow models
|
||||
import tensorflow as tf
|
||||
if pb: # https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt
|
||||
def wrap_frozen_graph(gd, inputs, outputs):
|
||||
x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped import
|
||||
return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs),
|
||||
tf.nest.map_structure(x.graph.as_graph_element, outputs))
|
||||
|
||||
graph_def = tf.Graph().as_graph_def()
|
||||
graph_def.ParseFromString(open(w, 'rb').read())
|
||||
frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0")
|
||||
elif saved_model:
|
||||
model = tf.keras.models.load_model(w)
|
||||
elif tflite:
|
||||
if "edgetpu" in w: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python
|
||||
import tflite_runtime.interpreter as tflri
|
||||
delegate = {'Linux': 'libedgetpu.so.1', # install libedgetpu https://coral.ai/software/#edgetpu-runtime
|
||||
'Darwin': 'libedgetpu.1.dylib',
|
||||
'Windows': 'edgetpu.dll'}[platform.system()]
|
||||
interpreter = tflri.Interpreter(model_path=w, experimental_delegates=[tflri.load_delegate(delegate)])
|
||||
else:
|
||||
interpreter = tf.lite.Interpreter(model_path=w) # load TFLite model
|
||||
interpreter.allocate_tensors() # allocate
|
||||
input_details = interpreter.get_input_details() # inputs
|
||||
output_details = interpreter.get_output_details() # outputs
|
||||
int8 = input_details[0]['dtype'] == np.uint8 # is TFLite quantized uint8 model
|
||||
device = select_device(device)
|
||||
model = DetectMultiBackend(weights, device=device, dnn=dnn)
|
||||
stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx
|
||||
imgsz = check_img_size(imgsz, s=stride) # check image size
|
||||
|
||||
# Half
|
||||
half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
|
||||
if pt:
|
||||
model.model.half() if half else model.model.float()
|
||||
|
||||
# Dataloader
|
||||
if webcam:
|
||||
view_img = check_imshow()
|
||||
cudnn.benchmark = True # set True to speed up constant image size inference
|
||||
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt)
|
||||
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit)
|
||||
bs = len(dataset) # batch_size
|
||||
else:
|
||||
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt)
|
||||
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)
|
||||
bs = 1 # batch_size
|
||||
vid_path, vid_writer = [None] * bs, [None] * bs
|
||||
|
||||
# Run inference
|
||||
if pt and device.type != 'cpu':
|
||||
model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.parameters()))) # run once
|
||||
model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.model.parameters()))) # warmup
|
||||
dt, seen = [0.0, 0.0, 0.0], 0
|
||||
for path, img, im0s, vid_cap, s in dataset:
|
||||
for path, im, im0s, vid_cap, s in dataset:
|
||||
t1 = time_sync()
|
||||
if onnx:
|
||||
img = img.astype('float32')
|
||||
else:
|
||||
img = torch.from_numpy(img).to(device)
|
||||
img = img.half() if half else img.float() # uint8 to fp16/32
|
||||
img /= 255 # 0 - 255 to 0.0 - 1.0
|
||||
if len(img.shape) == 3:
|
||||
img = img[None] # expand for batch dim
|
||||
im = torch.from_numpy(im).to(device)
|
||||
im = im.half() if half else im.float() # uint8 to fp16/32
|
||||
im /= 255 # 0 - 255 to 0.0 - 1.0
|
||||
if len(im.shape) == 3:
|
||||
im = im[None] # expand for batch dim
|
||||
t2 = time_sync()
|
||||
dt[0] += t2 - t1
|
||||
|
||||
# Inference
|
||||
if pt:
|
||||
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
|
||||
pred = model(img, augment=augment, visualize=visualize)[0]
|
||||
elif onnx:
|
||||
if dnn:
|
||||
net.setInput(img)
|
||||
pred = torch.tensor(net.forward())
|
||||
else:
|
||||
pred = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
|
||||
else: # tensorflow model (tflite, pb, saved_model)
|
||||
imn = img.permute(0, 2, 3, 1).cpu().numpy() # image in numpy
|
||||
if pb:
|
||||
pred = frozen_func(x=tf.constant(imn)).numpy()
|
||||
elif saved_model:
|
||||
pred = model(imn, training=False).numpy()
|
||||
elif tflite:
|
||||
if int8:
|
||||
scale, zero_point = input_details[0]['quantization']
|
||||
imn = (imn / scale + zero_point).astype(np.uint8) # de-scale
|
||||
interpreter.set_tensor(input_details[0]['index'], imn)
|
||||
interpreter.invoke()
|
||||
pred = interpreter.get_tensor(output_details[0]['index'])
|
||||
if int8:
|
||||
scale, zero_point = output_details[0]['quantization']
|
||||
pred = (pred.astype(np.float32) - zero_point) * scale # re-scale
|
||||
pred[..., 0] *= imgsz[1] # x
|
||||
pred[..., 1] *= imgsz[0] # y
|
||||
pred[..., 2] *= imgsz[1] # w
|
||||
pred[..., 3] *= imgsz[0] # h
|
||||
pred = torch.tensor(pred)
|
||||
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
|
||||
pred = model(im, augment=augment, visualize=visualize)
|
||||
t3 = time_sync()
|
||||
dt[1] += t3 - t2
|
||||
|
||||
@ -193,8 +121,7 @@ def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s)
|
||||
dt[2] += time_sync() - t3
|
||||
|
||||
# Second-stage classifier (optional)
|
||||
if classify:
|
||||
pred = apply_classifier(pred, modelc, img, im0s)
|
||||
# pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)
|
||||
|
||||
# Process predictions
|
||||
for i, det in enumerate(pred): # per image
|
||||
@ -206,15 +133,15 @@ def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s)
|
||||
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
|
||||
|
||||
p = Path(p) # to Path
|
||||
save_path = str(save_dir / p.name) # img.jpg
|
||||
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # img.txt
|
||||
s += '%gx%g ' % img.shape[2:] # print string
|
||||
save_path = str(save_dir / p.name) # im.jpg
|
||||
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt
|
||||
s += '%gx%g ' % im.shape[2:] # print string
|
||||
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh
|
||||
imc = im0.copy() if save_crop else im0 # for save_crop
|
||||
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
|
||||
if len(det):
|
||||
# Rescale boxes from img_size to im0 size
|
||||
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
|
||||
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
|
||||
|
||||
# Print results
|
||||
for c in det[:, -1].unique():
|
||||
|
@ -21,6 +21,7 @@ TensorFlow.js:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@ -54,7 +55,9 @@ def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:'
|
||||
f = file.with_suffix('.torchscript.pt')
|
||||
|
||||
ts = torch.jit.trace(model, im, strict=False)
|
||||
(optimize_for_mobile(ts) if optimize else ts).save(f)
|
||||
d = {"shape": im.shape, "stride": int(max(model.stride)), "names": model.names}
|
||||
extra_files = {'config.txt': json.dumps(d)} # torch._C.ExtraFilesMap()
|
||||
(optimize_for_mobile(ts) if optimize else ts).save(f, _extra_files=extra_files)
|
||||
|
||||
LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
|
||||
except Exception as e:
|
||||
|
134
models/common.py
134
models/common.py
@ -3,12 +3,14 @@
|
||||
Common modules
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
import warnings
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests
|
||||
@ -18,13 +20,11 @@ from PIL import Image
|
||||
from torch.cuda import amp
|
||||
|
||||
from utils.datasets import exif_transpose, letterbox
|
||||
from utils.general import (colorstr, increment_path, make_divisible, non_max_suppression, save_one_box, scale_coords,
|
||||
xyxy2xywh)
|
||||
from utils.plots import Annotator, colors
|
||||
from utils.general import (LOGGER, check_requirements, check_suffix, colorstr, increment_path, make_divisible,
|
||||
non_max_suppression, scale_coords, xywh2xyxy, xyxy2xywh)
|
||||
from utils.plots import Annotator, colors, save_one_box
|
||||
from utils.torch_utils import time_sync
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def autopad(k, p=None): # kernel, padding
|
||||
# Pad to 'same'
|
||||
@ -273,6 +273,128 @@ class Concat(nn.Module):
|
||||
return torch.cat(x, self.d)
|
||||
|
||||
|
||||
class DetectMultiBackend(nn.Module):
|
||||
# YOLOv5 MultiBackend class for python inference on various backends
|
||||
def __init__(self, weights='yolov5s.pt', device=None, dnn=True):
|
||||
# Usage:
|
||||
# PyTorch: weights = *.pt
|
||||
# TorchScript: *.torchscript.pt
|
||||
# CoreML: *.mlmodel
|
||||
# TensorFlow: *_saved_model
|
||||
# TensorFlow: *.pb
|
||||
# TensorFlow Lite: *.tflite
|
||||
# ONNX Runtime: *.onnx
|
||||
# OpenCV DNN: *.onnx with dnn=True
|
||||
super().__init__()
|
||||
w = str(weights[0] if isinstance(weights, list) else weights)
|
||||
suffix, suffixes = Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '', '.mlmodel']
|
||||
check_suffix(w, suffixes) # check weights have acceptable suffix
|
||||
pt, onnx, tflite, pb, saved_model, coreml = (suffix == x for x in suffixes) # backend booleans
|
||||
jit = pt and 'torchscript' in w.lower()
|
||||
stride, names = 64, [f'class{i}' for i in range(1000)] # assign defaults
|
||||
|
||||
if jit: # TorchScript
|
||||
LOGGER.info(f'Loading {w} for TorchScript inference...')
|
||||
extra_files = {'config.txt': ''} # model metadata
|
||||
model = torch.jit.load(w, _extra_files=extra_files)
|
||||
if extra_files['config.txt']:
|
||||
d = json.loads(extra_files['config.txt']) # extra_files dict
|
||||
stride, names = int(d['stride']), d['names']
|
||||
elif pt: # PyTorch
|
||||
from models.experimental import attempt_load # scoped to avoid circular import
|
||||
model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device)
|
||||
stride = int(model.stride.max()) # model stride
|
||||
names = model.module.names if hasattr(model, 'module') else model.names # get class names
|
||||
elif coreml: # CoreML *.mlmodel
|
||||
import coremltools as ct
|
||||
model = ct.models.MLModel(w)
|
||||
elif dnn: # ONNX OpenCV DNN
|
||||
LOGGER.info(f'Loading {w} for ONNX OpenCV DNN inference...')
|
||||
check_requirements(('opencv-python>=4.5.4',))
|
||||
net = cv2.dnn.readNetFromONNX(w)
|
||||
elif onnx: # ONNX Runtime
|
||||
LOGGER.info(f'Loading {w} for ONNX Runtime inference...')
|
||||
check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime'))
|
||||
import onnxruntime
|
||||
session = onnxruntime.InferenceSession(w, None)
|
||||
else: # TensorFlow model (TFLite, pb, saved_model)
|
||||
import tensorflow as tf
|
||||
if pb: # https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt
|
||||
def wrap_frozen_graph(gd, inputs, outputs):
|
||||
x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped
|
||||
return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs),
|
||||
tf.nest.map_structure(x.graph.as_graph_element, outputs))
|
||||
|
||||
LOGGER.info(f'Loading {w} for TensorFlow *.pb inference...')
|
||||
graph_def = tf.Graph().as_graph_def()
|
||||
graph_def.ParseFromString(open(w, 'rb').read())
|
||||
frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0")
|
||||
elif saved_model:
|
||||
LOGGER.info(f'Loading {w} for TensorFlow saved_model inference...')
|
||||
model = tf.keras.models.load_model(w)
|
||||
elif tflite: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python
|
||||
if 'edgetpu' in w.lower():
|
||||
LOGGER.info(f'Loading {w} for TensorFlow Edge TPU inference...')
|
||||
import tflite_runtime.interpreter as tfli
|
||||
delegate = {'Linux': 'libedgetpu.so.1', # install https://coral.ai/software/#edgetpu-runtime
|
||||
'Darwin': 'libedgetpu.1.dylib',
|
||||
'Windows': 'edgetpu.dll'}[platform.system()]
|
||||
interpreter = tfli.Interpreter(model_path=w, experimental_delegates=[tfli.load_delegate(delegate)])
|
||||
else:
|
||||
LOGGER.info(f'Loading {w} for TensorFlow Lite inference...')
|
||||
interpreter = tf.lite.Interpreter(model_path=w) # load TFLite model
|
||||
interpreter.allocate_tensors() # allocate
|
||||
input_details = interpreter.get_input_details() # inputs
|
||||
output_details = interpreter.get_output_details() # outputs
|
||||
self.__dict__.update(locals()) # assign all variables to self
|
||||
|
||||
def forward(self, im, augment=False, visualize=False, val=False):
|
||||
# YOLOv5 MultiBackend inference
|
||||
b, ch, h, w = im.shape # batch, channel, height, width
|
||||
if self.pt: # PyTorch
|
||||
y = self.model(im) if self.jit else self.model(im, augment=augment, visualize=visualize)
|
||||
return y if val else y[0]
|
||||
elif self.coreml: # CoreML *.mlmodel
|
||||
im = im.permute(0, 2, 3, 1).cpu().numpy() # torch BCHW to numpy BHWC shape(1,320,192,3)
|
||||
im = Image.fromarray((im[0] * 255).astype('uint8'))
|
||||
# im = im.resize((192, 320), Image.ANTIALIAS)
|
||||
y = self.model.predict({'image': im}) # coordinates are xywh normalized
|
||||
box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]]) # xyxy pixels
|
||||
conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float)
|
||||
y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)
|
||||
elif self.onnx: # ONNX
|
||||
im = im.cpu().numpy() # torch to numpy
|
||||
if self.dnn: # ONNX OpenCV DNN
|
||||
self.net.setInput(im)
|
||||
y = self.net.forward()
|
||||
else: # ONNX Runtime
|
||||
y = self.session.run([self.session.get_outputs()[0].name], {self.session.get_inputs()[0].name: im})[0]
|
||||
else: # TensorFlow model (TFLite, pb, saved_model)
|
||||
im = im.permute(0, 2, 3, 1).cpu().numpy() # torch BCHW to numpy BHWC shape(1,320,192,3)
|
||||
if self.pb:
|
||||
y = self.frozen_func(x=self.tf.constant(im)).numpy()
|
||||
elif self.saved_model:
|
||||
y = self.model(im, training=False).numpy()
|
||||
elif self.tflite:
|
||||
input, output = self.input_details[0], self.output_details[0]
|
||||
int8 = input['dtype'] == np.uint8 # is TFLite quantized uint8 model
|
||||
if int8:
|
||||
scale, zero_point = input['quantization']
|
||||
im = (im / scale + zero_point).astype(np.uint8) # de-scale
|
||||
self.interpreter.set_tensor(input['index'], im)
|
||||
self.interpreter.invoke()
|
||||
y = self.interpreter.get_tensor(output['index'])
|
||||
if int8:
|
||||
scale, zero_point = output['quantization']
|
||||
y = (y.astype(np.float32) - zero_point) * scale # re-scale
|
||||
y[..., 0] *= w # x
|
||||
y[..., 1] *= h # y
|
||||
y[..., 2] *= w # w
|
||||
y[..., 3] *= h # h
|
||||
y = torch.tensor(y)
|
||||
return (y, []) if val else y
|
||||
|
||||
|
||||
class AutoShape(nn.Module):
|
||||
# YOLOv5 input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS
|
||||
conf = 0.25 # NMS confidence threshold
|
||||
|
22
train.py
22
train.py
@ -5,9 +5,7 @@ Train a YOLOv5 model on a custom dataset
|
||||
Usage:
|
||||
$ python path/to/train.py --data coco128.yaml --weights yolov5s.pt --img 640
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
@ -41,10 +39,10 @@ from utils.autobatch import check_train_batch_size
|
||||
from utils.callbacks import Callbacks
|
||||
from utils.datasets import create_dataloader
|
||||
from utils.downloads import attempt_download
|
||||
from utils.general import (LOGGER, check_dataset, check_file, check_git_status, check_img_size, check_requirements,
|
||||
check_suffix, check_yaml, colorstr, get_latest_run, increment_path, init_seeds,
|
||||
intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods, one_cycle,
|
||||
print_args, print_mutation, strip_optimizer)
|
||||
from utils.general import (LOGGER, NCOLS, check_dataset, check_file, check_git_status, check_img_size,
|
||||
check_requirements, check_suffix, check_yaml, colorstr, get_latest_run, increment_path,
|
||||
init_seeds, intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods,
|
||||
one_cycle, print_args, print_mutation, strip_optimizer)
|
||||
from utils.loggers import Loggers
|
||||
from utils.loggers.wandb.wandb_utils import check_wandb_resume
|
||||
from utils.loss import ComputeLoss
|
||||
@ -201,8 +199,8 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
||||
|
||||
# DP mode
|
||||
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
|
||||
logging.warning('DP not recommended, instead use torch.distributed.run for best DDP Multi-GPU results.\n'
|
||||
'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
|
||||
LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n'
|
||||
'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
|
||||
model = torch.nn.DataParallel(model)
|
||||
|
||||
# SyncBatchNorm
|
||||
@ -214,7 +212,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
||||
train_loader, dataset = create_dataloader(train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls,
|
||||
hyp=hyp, augment=True, cache=opt.cache, rect=opt.rect, rank=LOCAL_RANK,
|
||||
workers=workers, image_weights=opt.image_weights, quad=opt.quad,
|
||||
prefix=colorstr('train: '))
|
||||
prefix=colorstr('train: '), shuffle=True)
|
||||
mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max()) # max label class
|
||||
nb = len(train_loader) # number of batches
|
||||
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'
|
||||
@ -268,7 +266,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
||||
stopper = EarlyStopping(patience=opt.patience)
|
||||
compute_loss = ComputeLoss(model) # init loss class
|
||||
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n'
|
||||
f'Using {train_loader.num_workers} dataloader workers\n'
|
||||
f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n'
|
||||
f"Logging results to {colorstr('bold', save_dir)}\n"
|
||||
f'Starting training for {epochs} epochs...')
|
||||
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
|
||||
@ -290,7 +288,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
||||
pbar = enumerate(train_loader)
|
||||
LOGGER.info(('\n' + '%10s' * 7) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'labels', 'img_size'))
|
||||
if RANK in [-1, 0]:
|
||||
pbar = tqdm(pbar, total=nb) # progress bar
|
||||
pbar = tqdm(pbar, total=nb, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
|
||||
optimizer.zero_grad()
|
||||
for i, (imgs, targets, paths, _) in pbar: # batch -------------------------------------------------------------
|
||||
ni = i + nb * epoch # number integrated batches (since train start)
|
||||
@ -462,7 +460,7 @@ def parse_opt(known=False):
|
||||
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
|
||||
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
|
||||
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
|
||||
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
|
||||
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
|
||||
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
|
||||
parser.add_argument('--name', default='exp', help='save to project/name')
|
||||
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
|
||||
|
36
tutorial.ipynb
vendored
36
tutorial.ipynb
vendored
@ -368,7 +368,7 @@
|
||||
"colab_type": "text"
|
||||
},
|
||||
"source": [
|
||||
"<a href=\"https://colab.research.google.com/github/ultralytics/yolov5/blob/master/tutorial.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||
"<a href=\"https://colab.research.google.com/github/ultralytics/yolov5/blob/update%2Fnotebook/tutorial.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -402,26 +402,24 @@
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "e2e839d5-d6fc-409c-e44c-0b0b6aa9319d"
|
||||
"outputId": "3809e5a9-dd41-4577-fe62-5531abf7cca2"
|
||||
},
|
||||
"source": [
|
||||
"!git clone https://github.com/ultralytics/yolov5 # clone repo\n",
|
||||
"!git clone https://github.com/ultralytics/yolov5 # clone\n",
|
||||
"%cd yolov5\n",
|
||||
"%pip install -qr requirements.txt # install dependencies\n",
|
||||
"%pip install -qr requirements.txt # install\n",
|
||||
"\n",
|
||||
"import torch\n",
|
||||
"from IPython.display import Image, clear_output # to display images\n",
|
||||
"\n",
|
||||
"clear_output()\n",
|
||||
"print(f\"Setup complete. Using torch {torch.__version__} ({torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else 'CPU'})\")"
|
||||
"from yolov5 import utils\n",
|
||||
"display = utils.notebook_init() # checks"
|
||||
],
|
||||
"execution_count": 11,
|
||||
"execution_count": 2,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
"name": "stdout",
|
||||
"text": [
|
||||
"Setup complete. Using torch 1.10.0+cu102 (Tesla V100-SXM2-16GB)\n"
|
||||
"YOLOv5 🚀 v6.0-48-g84a8099 torch 1.10.0+cu102 CUDA:0 (Tesla V100-SXM2-16GB, 16160MiB)\n",
|
||||
"Setup complete ✅\n"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -438,11 +436,11 @@
|
||||
"\n",
|
||||
"```shell\n",
|
||||
"python detect.py --source 0 # webcam\n",
|
||||
" file.jpg # image \n",
|
||||
" file.mp4 # video\n",
|
||||
" img.jpg # image \n",
|
||||
" vid.mp4 # video\n",
|
||||
" path/ # directory\n",
|
||||
" path/*.jpg # glob\n",
|
||||
" 'https://youtu.be/NUsoVlDFqZg' # YouTube\n",
|
||||
" 'https://youtu.be/Zgi9g1ksQHc' # YouTube\n",
|
||||
" 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream\n",
|
||||
"```"
|
||||
]
|
||||
@ -458,9 +456,9 @@
|
||||
},
|
||||
"source": [
|
||||
"!python detect.py --weights yolov5s.pt --img 640 --conf 0.25 --source data/images\n",
|
||||
"Image(filename='runs/detect/exp/zidane.jpg', width=600)"
|
||||
"display.Image(filename='runs/detect/exp/zidane.jpg', width=600)"
|
||||
],
|
||||
"execution_count": 17,
|
||||
"execution_count": null,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
@ -537,7 +535,7 @@
|
||||
"torch.hub.download_url_to_file('https://ultralytics.com/assets/coco2017val.zip', 'tmp.zip')\n",
|
||||
"!unzip -q tmp.zip -d ../datasets && rm tmp.zip"
|
||||
],
|
||||
"execution_count": 18,
|
||||
"execution_count": null,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "display_data",
|
||||
@ -568,7 +566,7 @@
|
||||
"# Run YOLOv5x on COCO val\n",
|
||||
"!python val.py --weights yolov5x.pt --data coco.yaml --img 640 --iou 0.65 --half"
|
||||
],
|
||||
"execution_count": 19,
|
||||
"execution_count": null,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
@ -726,7 +724,7 @@
|
||||
"# Train YOLOv5s on COCO128 for 3 epochs\n",
|
||||
"!python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache"
|
||||
],
|
||||
"execution_count": 24,
|
||||
"execution_count": null,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
|
@ -0,0 +1,18 @@
|
||||
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
|
||||
"""
|
||||
utils/initialization
|
||||
"""
|
||||
|
||||
|
||||
def notebook_init():
|
||||
# For YOLOv5 notebooks
|
||||
print('Checking setup...')
|
||||
from IPython import display # to display images and clear console output
|
||||
|
||||
from utils.general import emojis
|
||||
from utils.torch_utils import select_device # YOLOv5 imports
|
||||
|
||||
display.clear_output()
|
||||
select_device(newline=False)
|
||||
print(emojis('Setup complete ✅'))
|
||||
return display
|
@ -3,14 +3,13 @@
|
||||
Image augmentation functions
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from utils.general import check_version, colorstr, resample_segments, segment2box
|
||||
from utils.general import LOGGER, check_version, colorstr, resample_segments, segment2box
|
||||
from utils.metrics import bbox_ioa
|
||||
|
||||
|
||||
@ -32,11 +31,11 @@ class Albumentations:
|
||||
A.ImageCompression(quality_lower=75, p=0.0)],
|
||||
bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
|
||||
|
||||
logging.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p))
|
||||
LOGGER.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p))
|
||||
except ImportError: # package not installed, skip
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.info(colorstr('albumentations: ') + f'{e}')
|
||||
LOGGER.info(colorstr('albumentations: ') + f'{e}')
|
||||
|
||||
def __call__(self, im, labels, p=1.0):
|
||||
if self.transform and random.random() < p:
|
||||
|
@ -10,7 +10,9 @@ import torch
|
||||
import yaml
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.general import colorstr
|
||||
from utils.general import LOGGER, colorstr, emojis
|
||||
|
||||
PREFIX = colorstr('AutoAnchor: ')
|
||||
|
||||
|
||||
def check_anchor_order(m):
|
||||
@ -19,14 +21,12 @@ def check_anchor_order(m):
|
||||
da = a[-1] - a[0] # delta a
|
||||
ds = m.stride[-1] - m.stride[0] # delta s
|
||||
if da.sign() != ds.sign(): # same order
|
||||
print('Reversing anchor order')
|
||||
LOGGER.info(f'{PREFIX}Reversing anchor order')
|
||||
m.anchors[:] = m.anchors.flip(0)
|
||||
|
||||
|
||||
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
# Check anchor fit to data, recompute if necessary
|
||||
prefix = colorstr('autoanchor: ')
|
||||
print(f'\n{prefix}Analyzing anchors... ', end='')
|
||||
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
||||
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
||||
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
||||
@ -42,23 +42,24 @@ def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
|
||||
anchors = m.anchors.clone() * m.stride.to(m.anchors.device).view(-1, 1, 1) # current anchors
|
||||
bpr, aat = metric(anchors.cpu().view(-1, 2))
|
||||
print(f'anchors/target = {aat:.2f}, Best Possible Recall (BPR) = {bpr:.4f}', end='')
|
||||
if bpr < 0.98: # threshold to recompute
|
||||
print('. Attempting to improve anchors, please wait...')
|
||||
s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). '
|
||||
if bpr > 0.98: # threshold to recompute
|
||||
LOGGER.info(emojis(f'{s}Current anchors are a good fit to dataset ✅'))
|
||||
else:
|
||||
LOGGER.info(emojis(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...'))
|
||||
na = m.anchors.numel() // 2 # number of anchors
|
||||
try:
|
||||
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
||||
except Exception as e:
|
||||
print(f'{prefix}ERROR: {e}')
|
||||
LOGGER.info(f'{PREFIX}ERROR: {e}')
|
||||
new_bpr = metric(anchors)[0]
|
||||
if new_bpr > bpr: # replace anchors
|
||||
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
||||
check_anchor_order(m)
|
||||
print(f'{prefix}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
||||
LOGGER.info(f'{PREFIX}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
||||
else:
|
||||
print(f'{prefix}Original anchors better than new anchors. Proceeding with original anchors.')
|
||||
print('') # newline
|
||||
LOGGER.info(f'{PREFIX}Original anchors better than new anchors. Proceeding with original anchors.')
|
||||
|
||||
|
||||
def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
||||
@ -81,7 +82,6 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
|
||||
from scipy.cluster.vq import kmeans
|
||||
|
||||
thr = 1 / thr
|
||||
prefix = colorstr('autoanchor: ')
|
||||
|
||||
def metric(k, wh): # compute metrics
|
||||
r = wh[:, None] / k[None]
|
||||
@ -93,15 +93,17 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
|
||||
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
||||
return (best * (best > thr).float()).mean() # fitness
|
||||
|
||||
def print_results(k):
|
||||
def print_results(k, verbose=True):
|
||||
k = k[np.argsort(k.prod(1))] # sort small to large
|
||||
x, best = metric(k, wh0)
|
||||
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
||||
print(f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr')
|
||||
print(f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, '
|
||||
f'past_thr={x[x > thr].mean():.3f}-mean: ', end='')
|
||||
s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \
|
||||
f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \
|
||||
f'past_thr={x[x > thr].mean():.3f}-mean: '
|
||||
for i, x in enumerate(k):
|
||||
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
||||
s += '%i,%i, ' % (round(x[0]), round(x[1]))
|
||||
if verbose:
|
||||
LOGGER.info(s[:-2])
|
||||
return k
|
||||
|
||||
if isinstance(dataset, str): # *.yaml file
|
||||
@ -117,19 +119,19 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
|
||||
# Filter
|
||||
i = (wh0 < 3.0).any(1).sum()
|
||||
if i:
|
||||
print(f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
|
||||
LOGGER.info(f'{PREFIX}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
|
||||
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
||||
# wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
|
||||
|
||||
# Kmeans calculation
|
||||
print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
|
||||
LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...')
|
||||
s = wh.std(0) # sigmas for whitening
|
||||
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
||||
assert len(k) == n, f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}'
|
||||
assert len(k) == n, f'{PREFIX}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}'
|
||||
k *= s
|
||||
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
||||
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
||||
k = print_results(k)
|
||||
k = print_results(k, verbose=False)
|
||||
|
||||
# Plot
|
||||
# k, d = [None] * 20, [None] * 20
|
||||
@ -146,7 +148,7 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
|
||||
# Evolve
|
||||
npr = np.random
|
||||
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
||||
pbar = tqdm(range(gen), desc=f'{prefix}Evolving anchors with Genetic Algorithm:') # progress bar
|
||||
pbar = tqdm(range(gen), desc=f'{PREFIX}Evolving anchors with Genetic Algorithm:') # progress bar
|
||||
for _ in pbar:
|
||||
v = np.ones(sh)
|
||||
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
||||
@ -155,8 +157,8 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
|
||||
fg = anchor_fitness(kg)
|
||||
if fg > f:
|
||||
f, k = fg, kg.copy()
|
||||
pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
|
||||
pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
|
||||
if verbose:
|
||||
print_results(k)
|
||||
print_results(k, verbose)
|
||||
|
||||
return print_results(k)
|
||||
|
@ -9,7 +9,7 @@ import numpy as np
|
||||
import torch
|
||||
from torch.cuda import amp
|
||||
|
||||
from utils.general import colorstr
|
||||
from utils.general import LOGGER, colorstr
|
||||
from utils.torch_utils import profile
|
||||
|
||||
|
||||
@ -27,11 +27,11 @@ def autobatch(model, imgsz=640, fraction=0.9, batch_size=16):
|
||||
# model = torch.hub.load('ultralytics/yolov5', 'yolov5s', autoshape=False)
|
||||
# print(autobatch(model))
|
||||
|
||||
prefix = colorstr('autobatch: ')
|
||||
print(f'{prefix}Computing optimal batch size for --imgsz {imgsz}')
|
||||
prefix = colorstr('AutoBatch: ')
|
||||
LOGGER.info(f'{prefix}Computing optimal batch size for --imgsz {imgsz}')
|
||||
device = next(model.parameters()).device # get model device
|
||||
if device.type == 'cpu':
|
||||
print(f'{prefix}CUDA not detected, using default CPU batch-size {batch_size}')
|
||||
LOGGER.info(f'{prefix}CUDA not detected, using default CPU batch-size {batch_size}')
|
||||
return batch_size
|
||||
|
||||
d = str(device).upper() # 'CUDA:0'
|
||||
@ -40,18 +40,18 @@ def autobatch(model, imgsz=640, fraction=0.9, batch_size=16):
|
||||
r = torch.cuda.memory_reserved(device) / 1024 ** 3 # (GiB)
|
||||
a = torch.cuda.memory_allocated(device) / 1024 ** 3 # (GiB)
|
||||
f = t - (r + a) # free inside reserved
|
||||
print(f'{prefix}{d} ({properties.name}) {t:.2f}G total, {r:.2f}G reserved, {a:.2f}G allocated, {f:.2f}G free')
|
||||
LOGGER.info(f'{prefix}{d} ({properties.name}) {t:.2f}G total, {r:.2f}G reserved, {a:.2f}G allocated, {f:.2f}G free')
|
||||
|
||||
batch_sizes = [1, 2, 4, 8, 16]
|
||||
try:
|
||||
img = [torch.zeros(b, 3, imgsz, imgsz) for b in batch_sizes]
|
||||
y = profile(img, model, n=3, device=device)
|
||||
except Exception as e:
|
||||
print(f'{prefix}{e}')
|
||||
LOGGER.warning(f'{prefix}{e}')
|
||||
|
||||
y = [x[2] for x in y if x] # memory [2]
|
||||
batch_sizes = batch_sizes[:len(y)]
|
||||
p = np.polyfit(batch_sizes, y, deg=1) # first degree polynomial fit
|
||||
b = int((f * fraction - p[1]) / p[0]) # y intercept (optimal batch size)
|
||||
print(f'{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%)')
|
||||
LOGGER.info(f'{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%)')
|
||||
return b
|
||||
|
@ -6,7 +6,6 @@ Dataloaders and dataset utils
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
@ -23,7 +22,7 @@ import torch
|
||||
import torch.nn.functional as F
|
||||
import yaml
|
||||
from PIL import ExifTags, Image, ImageOps
|
||||
from torch.utils.data import Dataset
|
||||
from torch.utils.data import DataLoader, Dataset, dataloader, distributed
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.augmentations import Albumentations, augment_hsv, copy_paste, letterbox, mixup, random_perspective
|
||||
@ -35,6 +34,7 @@ from utils.torch_utils import torch_distributed_zero_first
|
||||
HELP_URL = 'https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data'
|
||||
IMG_FORMATS = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo'] # acceptable image suffixes
|
||||
VID_FORMATS = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv'] # acceptable video suffixes
|
||||
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) # DPP
|
||||
NUM_THREADS = min(8, os.cpu_count()) # number of multiprocessing threads
|
||||
|
||||
# Get orientation exif tag
|
||||
@ -93,13 +93,15 @@ def exif_transpose(image):
|
||||
|
||||
|
||||
def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=None, augment=False, cache=False, pad=0.0,
|
||||
rect=False, rank=-1, workers=8, image_weights=False, quad=False, prefix=''):
|
||||
# Make sure only the first process in DDP process the dataset first, and the following others can use the cache
|
||||
with torch_distributed_zero_first(rank):
|
||||
rect=False, rank=-1, workers=8, image_weights=False, quad=False, prefix='', shuffle=False):
|
||||
if rect and shuffle:
|
||||
LOGGER.warning('WARNING: --rect is incompatible with DataLoader shuffle, setting shuffle=False')
|
||||
shuffle = False
|
||||
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
|
||||
dataset = LoadImagesAndLabels(path, imgsz, batch_size,
|
||||
augment=augment, # augment images
|
||||
hyp=hyp, # augmentation hyperparameters
|
||||
rect=rect, # rectangular training
|
||||
augment=augment, # augmentation
|
||||
hyp=hyp, # hyperparameters
|
||||
rect=rect, # rectangular batches
|
||||
cache_images=cache,
|
||||
single_cls=single_cls,
|
||||
stride=int(stride),
|
||||
@ -108,20 +110,19 @@ def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=Non
|
||||
prefix=prefix)
|
||||
|
||||
batch_size = min(batch_size, len(dataset))
|
||||
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, workers]) # number of workers
|
||||
sampler = torch.utils.data.distributed.DistributedSampler(dataset) if rank != -1 else None
|
||||
loader = torch.utils.data.DataLoader if image_weights else InfiniteDataLoader
|
||||
# Use torch.utils.data.DataLoader() if dataset.properties will update during training else InfiniteDataLoader()
|
||||
dataloader = loader(dataset,
|
||||
batch_size=batch_size,
|
||||
num_workers=nw,
|
||||
sampler=sampler,
|
||||
pin_memory=True,
|
||||
collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn)
|
||||
return dataloader, dataset
|
||||
nw = min([os.cpu_count() // WORLD_SIZE, batch_size if batch_size > 1 else 0, workers]) # number of workers
|
||||
sampler = None if rank == -1 else distributed.DistributedSampler(dataset, shuffle=shuffle)
|
||||
loader = DataLoader if image_weights else InfiniteDataLoader # only DataLoader allows for attribute updates
|
||||
return loader(dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=shuffle and sampler is None,
|
||||
num_workers=nw,
|
||||
sampler=sampler,
|
||||
pin_memory=True,
|
||||
collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn), dataset
|
||||
|
||||
|
||||
class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader):
|
||||
class InfiniteDataLoader(dataloader.DataLoader):
|
||||
""" Dataloader that reuses workers
|
||||
|
||||
Uses same syntax as vanilla DataLoader
|
||||
@ -335,7 +336,7 @@ class LoadStreams:
|
||||
if success:
|
||||
self.imgs[i] = im
|
||||
else:
|
||||
LOGGER.warn('WARNING: Video stream unresponsive, please check your IP camera connection.')
|
||||
LOGGER.warning('WARNING: Video stream unresponsive, please check your IP camera connection.')
|
||||
self.imgs[i] *= 0
|
||||
cap.open(stream) # re-open stream if signal was lost
|
||||
time.sleep(1 / self.fps[i]) # wait time
|
||||
@ -427,7 +428,7 @@ class LoadImagesAndLabels(Dataset):
|
||||
d = f"Scanning '{cache_path}' images and labels... {nf} found, {nm} missing, {ne} empty, {nc} corrupted"
|
||||
tqdm(None, desc=prefix + d, total=n, initial=n) # display cache results
|
||||
if cache['msgs']:
|
||||
logging.info('\n'.join(cache['msgs'])) # display warnings
|
||||
LOGGER.info('\n'.join(cache['msgs'])) # display warnings
|
||||
assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {HELP_URL}'
|
||||
|
||||
# Read cache
|
||||
@ -525,9 +526,9 @@ class LoadImagesAndLabels(Dataset):
|
||||
|
||||
pbar.close()
|
||||
if msgs:
|
||||
logging.info('\n'.join(msgs))
|
||||
LOGGER.info('\n'.join(msgs))
|
||||
if nf == 0:
|
||||
logging.info(f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}')
|
||||
LOGGER.warning(f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}')
|
||||
x['hash'] = get_hash(self.label_files + self.img_files)
|
||||
x['results'] = nf, nm, ne, nc, len(self.img_files)
|
||||
x['msgs'] = msgs # warnings
|
||||
@ -535,9 +536,9 @@ class LoadImagesAndLabels(Dataset):
|
||||
try:
|
||||
np.save(path, x) # save cache for next time
|
||||
path.with_suffix('.cache.npy').rename(path) # remove .npy suffix
|
||||
logging.info(f'{prefix}New cache created: {path}')
|
||||
LOGGER.info(f'{prefix}New cache created: {path}')
|
||||
except Exception as e:
|
||||
logging.info(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # path not writeable
|
||||
LOGGER.warning(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # not writeable
|
||||
return x
|
||||
|
||||
def __len__(self):
|
||||
@ -914,10 +915,12 @@ def verify_image_label(args):
|
||||
assert l.shape[1] == 5, f'labels require 5 columns, {l.shape[1]} columns detected'
|
||||
assert (l >= 0).all(), f'negative label values {l[l < 0]}'
|
||||
assert (l[:, 1:] <= 1).all(), f'non-normalized or out of bounds coordinates {l[:, 1:][l[:, 1:] > 1]}'
|
||||
l = np.unique(l, axis=0) # remove duplicate rows
|
||||
if len(l) < nl:
|
||||
segments = np.unique(segments, axis=0)
|
||||
msg = f'{prefix}WARNING: {im_file}: {nl - len(l)} duplicate labels removed'
|
||||
_, i = np.unique(l, axis=0, return_index=True)
|
||||
if len(i) < nl: # duplicate row check
|
||||
l = l[i] # remove duplicates
|
||||
if segments:
|
||||
segments = segments[i]
|
||||
msg = f'{prefix}WARNING: {im_file}: {nl - len(i)} duplicate labels removed'
|
||||
else:
|
||||
ne = 1 # label empty
|
||||
l = np.zeros((0, 5), dtype=np.float32)
|
||||
@ -964,7 +967,7 @@ def dataset_stats(path='coco128.yaml', autodownload=False, verbose=False, profil
|
||||
r = max_dim / max(im.height, im.width) # ratio
|
||||
if r < 1.0: # image too large
|
||||
im = im.resize((int(im.width * r), int(im.height * r)))
|
||||
im.save(f_new, quality=75) # save
|
||||
im.save(f_new, 'JPEG', quality=75, optimize=True) # save
|
||||
except Exception as e: # use OpenCV
|
||||
print(f'WARNING: HUB ops PIL failure {f}: {e}')
|
||||
im = cv2.imread(f)
|
||||
|
@ -11,6 +11,7 @@ import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import time
|
||||
import urllib
|
||||
@ -45,7 +46,7 @@ ROOT = FILE.parents[1] # YOLOv5 root directory
|
||||
def set_logging(name=None, verbose=True):
|
||||
# Sets level and returns logger
|
||||
rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO if (verbose and rank in (-1, 0)) else logging.WARN)
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO if (verbose and rank in (-1, 0)) else logging.WARNING)
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
@ -264,7 +265,8 @@ def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), insta
|
||||
if isinstance(requirements, (str, Path)): # requirements.txt file
|
||||
file = Path(requirements)
|
||||
assert file.exists(), f"{prefix} {file.resolve()} not found, check failed."
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude]
|
||||
with file.open() as f:
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude]
|
||||
else: # list or tuple of packages
|
||||
requirements = [x for x in requirements if x not in exclude]
|
||||
|
||||
@ -785,7 +787,8 @@ def print_mutation(results, hyp, save_dir, bucket):
|
||||
|
||||
|
||||
def apply_classifier(x, model, img, im0):
|
||||
# Apply a second stage classifier to yolo outputs
|
||||
# Apply a second stage classifier to YOLO outputs
|
||||
# Example model = torchvision.models.__dict__['efficientnet_b0'](pretrained=True).to(device).eval()
|
||||
im0 = [im0] if isinstance(im0, np.ndarray) else im0
|
||||
for i, d in enumerate(x): # per image
|
||||
if d is not None and len(d):
|
||||
@ -819,21 +822,6 @@ def apply_classifier(x, model, img, im0):
|
||||
return x
|
||||
|
||||
|
||||
def save_one_box(xyxy, im, file='image.jpg', gain=1.02, pad=10, square=False, BGR=False, save=True):
|
||||
# Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop
|
||||
xyxy = torch.tensor(xyxy).view(-1, 4)
|
||||
b = xyxy2xywh(xyxy) # boxes
|
||||
if square:
|
||||
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square
|
||||
b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad
|
||||
xyxy = xywh2xyxy(b).long()
|
||||
clip_coords(xyxy, im.shape)
|
||||
crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)]
|
||||
if save:
|
||||
cv2.imwrite(str(increment_path(file, mkdir=True).with_suffix('.jpg')), crop)
|
||||
return crop
|
||||
|
||||
|
||||
def increment_path(path, exist_ok=False, sep='', mkdir=False):
|
||||
# Increment file or directory path, i.e. runs/exp --> runs/exp{sep}2, runs/exp{sep}3, ... etc.
|
||||
path = Path(path) # os-agnostic
|
||||
@ -847,3 +835,7 @@ def increment_path(path, exist_ok=False, sep='', mkdir=False):
|
||||
if mkdir:
|
||||
path.mkdir(parents=True, exist_ok=True) # make directory
|
||||
return path
|
||||
|
||||
|
||||
# Variables
|
||||
NCOLS = 0 if is_docker() else shutil.get_terminal_size().columns # terminal window size
|
||||
|
@ -2,11 +2,15 @@ import argparse
|
||||
|
||||
from wandb_utils import WandbLogger
|
||||
|
||||
from utils.general import LOGGER
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def create_dataset_artifact(opt):
|
||||
logger = WandbLogger(opt, None, job_type='Dataset Creation') # TODO: return value unused
|
||||
if not logger.wandb:
|
||||
LOGGER.info("install wandb using `pip install wandb` to log the dataset")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -17,7 +17,7 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT)) # add ROOT to PATH
|
||||
|
||||
from utils.datasets import LoadImagesAndLabels, img2label_paths
|
||||
from utils.general import check_dataset, check_file
|
||||
from utils.general import LOGGER, check_dataset, check_file
|
||||
|
||||
try:
|
||||
import wandb
|
||||
@ -203,7 +203,7 @@ class WandbLogger():
|
||||
config_path = self.log_dataset_artifact(opt.data,
|
||||
opt.single_cls,
|
||||
'YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem)
|
||||
print("Created dataset config file ", config_path)
|
||||
LOGGER.info(f"Created dataset config file {config_path}")
|
||||
with open(config_path, errors='ignore') as f:
|
||||
wandb_data_dict = yaml.safe_load(f)
|
||||
return wandb_data_dict
|
||||
@ -316,7 +316,7 @@ class WandbLogger():
|
||||
model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
|
||||
wandb.log_artifact(model_artifact,
|
||||
aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
|
||||
print("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):
|
||||
"""
|
||||
@ -368,7 +368,7 @@ class WandbLogger():
|
||||
Useful for - referencing artifacts for evaluation.
|
||||
"""
|
||||
self.val_table_path_map = {}
|
||||
print("Mapping dataset")
|
||||
LOGGER.info("Mapping dataset")
|
||||
for i, data in enumerate(tqdm(self.val_table.data)):
|
||||
self.val_table_path_map[data[3]] = data[0]
|
||||
|
||||
@ -488,7 +488,13 @@ class WandbLogger():
|
||||
with all_logging_disabled():
|
||||
if self.bbox_media_panel_images:
|
||||
self.log_dict["BoundingBoxDebugger"] = self.bbox_media_panel_images
|
||||
wandb.log(self.log_dict)
|
||||
try:
|
||||
wandb.log(self.log_dict)
|
||||
except BaseException as e:
|
||||
LOGGER.info(f"An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}")
|
||||
self.wandb_run.finish()
|
||||
self.wandb_run = None
|
||||
|
||||
self.log_dict = {}
|
||||
self.bbox_media_panel_images = []
|
||||
if self.result_artifact:
|
||||
|
136
utils/plots.py
136
utils/plots.py
@ -17,7 +17,8 @@ import seaborn as sn
|
||||
import torch
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from utils.general import is_ascii, is_chinese, user_config_dir, xywh2xyxy, xyxy2xywh
|
||||
from utils.general import (LOGGER, Timeout, check_requirements, clip_coords, increment_path, is_ascii, is_chinese,
|
||||
try_except, user_config_dir, xywh2xyxy, xyxy2xywh)
|
||||
from utils.metrics import fitness
|
||||
|
||||
# Settings
|
||||
@ -58,7 +59,10 @@ def check_font(font='Arial.ttf', size=10):
|
||||
url = "https://ultralytics.com/assets/" + font.name
|
||||
print(f'Downloading {url} to {font}...')
|
||||
torch.hub.download_url_to_file(url, str(font), progress=False)
|
||||
return ImageFont.truetype(str(font), size)
|
||||
try:
|
||||
return ImageFont.truetype(str(font), size)
|
||||
except TypeError:
|
||||
check_requirements('Pillow>=8.4.0') # known issue https://github.com/ultralytics/yolov5/issues/5374
|
||||
|
||||
|
||||
class Annotator:
|
||||
@ -117,6 +121,33 @@ class Annotator:
|
||||
return np.asarray(self.im)
|
||||
|
||||
|
||||
def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')):
|
||||
"""
|
||||
x: Features to be visualized
|
||||
module_type: Module type
|
||||
stage: Module stage within model
|
||||
n: Maximum number of feature maps to plot
|
||||
save_dir: Directory to save results
|
||||
"""
|
||||
if 'Detect' not in module_type:
|
||||
batch, channels, height, width = x.shape # batch, channels, height, width
|
||||
if height > 1 and width > 1:
|
||||
f = f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename
|
||||
|
||||
blocks = torch.chunk(x[0].cpu(), channels, dim=0) # select batch index 0, block by channels
|
||||
n = min(n, channels) # number of plots
|
||||
fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True) # 8 rows x n/8 cols
|
||||
ax = ax.ravel()
|
||||
plt.subplots_adjust(wspace=0.05, hspace=0.05)
|
||||
for i in range(n):
|
||||
ax[i].imshow(blocks[i].squeeze()) # cmap='gray'
|
||||
ax[i].axis('off')
|
||||
|
||||
print(f'Saving {save_dir / f}... ({n}/{channels})')
|
||||
plt.savefig(save_dir / f, dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
|
||||
def hist2d(x, y, n=100):
|
||||
# 2d histogram used in labels.png and evolve.png
|
||||
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
||||
@ -293,9 +324,11 @@ def plot_val_study(file='', dir='', x=None): # from utils.plots import *; plot_
|
||||
plt.savefig(f, dpi=300)
|
||||
|
||||
|
||||
@try_except # known issue https://github.com/ultralytics/yolov5/issues/5395
|
||||
@Timeout(30) # known issue https://github.com/ultralytics/yolov5/issues/5611
|
||||
def plot_labels(labels, names=(), save_dir=Path('')):
|
||||
# plot dataset labels
|
||||
print('Plotting labels... ')
|
||||
LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ")
|
||||
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
||||
nc = int(c.max() + 1) # number of classes
|
||||
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
||||
@ -337,37 +370,6 @@ def plot_labels(labels, names=(), save_dir=Path('')):
|
||||
plt.close()
|
||||
|
||||
|
||||
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
|
||||
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
|
||||
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
|
||||
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
|
||||
files = list(Path(save_dir).glob('frames*.txt'))
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
|
||||
n = results.shape[1] # number of rows
|
||||
x = np.arange(start, min(stop, n) if stop else n)
|
||||
results = results[:, x]
|
||||
t = (results[0] - results[0].min()) # set t0=0s
|
||||
results[0] = x
|
||||
for i, a in enumerate(ax):
|
||||
if i < len(results):
|
||||
label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
|
||||
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
|
||||
a.set_title(s[i])
|
||||
a.set_xlabel('time (s)')
|
||||
# if fi == len(files) - 1:
|
||||
# a.set_ylim(bottom=0)
|
||||
for side in ['top', 'right']:
|
||||
a.spines[side].set_visible(False)
|
||||
else:
|
||||
a.remove()
|
||||
except Exception as e:
|
||||
print(f'Warning: Plotting error for {f}; {e}')
|
||||
ax[1].legend()
|
||||
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
|
||||
|
||||
|
||||
def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; plot_evolve()
|
||||
# Plot evolve.csv hyp evolution results
|
||||
evolve_csv = Path(evolve_csv)
|
||||
@ -420,28 +422,48 @@ def plot_results(file='path/to/results.csv', dir=''):
|
||||
plt.close()
|
||||
|
||||
|
||||
def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')):
|
||||
"""
|
||||
x: Features to be visualized
|
||||
module_type: Module type
|
||||
stage: Module stage within model
|
||||
n: Maximum number of feature maps to plot
|
||||
save_dir: Directory to save results
|
||||
"""
|
||||
if 'Detect' not in module_type:
|
||||
batch, channels, height, width = x.shape # batch, channels, height, width
|
||||
if height > 1 and width > 1:
|
||||
f = f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename
|
||||
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
|
||||
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
|
||||
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
|
||||
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
|
||||
files = list(Path(save_dir).glob('frames*.txt'))
|
||||
for fi, f in enumerate(files):
|
||||
try:
|
||||
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
|
||||
n = results.shape[1] # number of rows
|
||||
x = np.arange(start, min(stop, n) if stop else n)
|
||||
results = results[:, x]
|
||||
t = (results[0] - results[0].min()) # set t0=0s
|
||||
results[0] = x
|
||||
for i, a in enumerate(ax):
|
||||
if i < len(results):
|
||||
label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
|
||||
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
|
||||
a.set_title(s[i])
|
||||
a.set_xlabel('time (s)')
|
||||
# if fi == len(files) - 1:
|
||||
# a.set_ylim(bottom=0)
|
||||
for side in ['top', 'right']:
|
||||
a.spines[side].set_visible(False)
|
||||
else:
|
||||
a.remove()
|
||||
except Exception as e:
|
||||
print(f'Warning: Plotting error for {f}; {e}')
|
||||
ax[1].legend()
|
||||
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
|
||||
|
||||
blocks = torch.chunk(x[0].cpu(), channels, dim=0) # select batch index 0, block by channels
|
||||
n = min(n, channels) # number of plots
|
||||
fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True) # 8 rows x n/8 cols
|
||||
ax = ax.ravel()
|
||||
plt.subplots_adjust(wspace=0.05, hspace=0.05)
|
||||
for i in range(n):
|
||||
ax[i].imshow(blocks[i].squeeze()) # cmap='gray'
|
||||
ax[i].axis('off')
|
||||
|
||||
print(f'Saving {save_dir / f}... ({n}/{channels})')
|
||||
plt.savefig(save_dir / f, dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
def save_one_box(xyxy, im, file='image.jpg', gain=1.02, pad=10, square=False, BGR=False, save=True):
|
||||
# Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop
|
||||
xyxy = torch.tensor(xyxy).view(-1, 4)
|
||||
b = xyxy2xywh(xyxy) # boxes
|
||||
if square:
|
||||
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square
|
||||
b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad
|
||||
xyxy = xywh2xyxy(b).long()
|
||||
clip_coords(xyxy, im.shape)
|
||||
crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)]
|
||||
if save:
|
||||
file.parent.mkdir(parents=True, exist_ok=True) # make directory
|
||||
cv2.imwrite(str(increment_path(file).with_suffix('.jpg')), crop)
|
||||
return crop
|
||||
|
@ -4,7 +4,6 @@ PyTorch utils
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
@ -18,7 +17,6 @@ import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
|
||||
from utils.general import LOGGER
|
||||
|
||||
@ -55,7 +53,7 @@ def git_describe(path=Path(__file__).parent): # path must be a directory
|
||||
return '' # not a git repository
|
||||
|
||||
|
||||
def select_device(device='', batch_size=None):
|
||||
def select_device(device='', batch_size=None, newline=True):
|
||||
# device = 'cpu' or '0' or '0,1,2,3'
|
||||
s = f'YOLOv5 🚀 {git_describe() or date_modified()} torch {torch.__version__} ' # string
|
||||
device = str(device).strip().lower().replace('cuda:', '') # to string, 'cuda:0' to '0'
|
||||
@ -79,6 +77,8 @@ def select_device(device='', batch_size=None):
|
||||
else:
|
||||
s += 'CPU\n'
|
||||
|
||||
if not newline:
|
||||
s = s.rstrip()
|
||||
LOGGER.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
||||
return torch.device('cuda:0' if cuda else 'cpu')
|
||||
|
||||
@ -100,7 +100,6 @@ def profile(input, ops, n=10, device=None):
|
||||
# profile(input, [m1, m2], n=100) # profile over 100 iterations
|
||||
|
||||
results = []
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
device = device or select_device()
|
||||
print(f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}"
|
||||
f"{'input':>24s}{'output':>24s}")
|
||||
@ -237,25 +236,6 @@ def model_info(model, verbose=False, img_size=640):
|
||||
LOGGER.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
||||
|
||||
|
||||
def load_classifier(name='resnet101', n=2):
|
||||
# Loads a pretrained model reshaped to n-class output
|
||||
model = torchvision.models.__dict__[name](pretrained=True)
|
||||
|
||||
# ResNet model properties
|
||||
# input_size = [3, 224, 224]
|
||||
# input_space = 'RGB'
|
||||
# input_range = [0, 1]
|
||||
# mean = [0.485, 0.456, 0.406]
|
||||
# std = [0.229, 0.224, 0.225]
|
||||
|
||||
# Reshape output to n classes
|
||||
filters = model.fc.weight.shape[1]
|
||||
model.fc.bias = nn.Parameter(torch.zeros(n), requires_grad=True)
|
||||
model.fc.weight = nn.Parameter(torch.zeros(n, filters), requires_grad=True)
|
||||
model.fc.out_features = n
|
||||
return model
|
||||
|
||||
|
||||
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
|
||||
# scales img(bs,3,y,x) by ratio constrained to gs-multiple
|
||||
if ratio == 1.0:
|
||||
|
116
val.py
116
val.py
@ -23,10 +23,10 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT)) # add ROOT to PATH
|
||||
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
|
||||
|
||||
from models.experimental import attempt_load
|
||||
from models.common import DetectMultiBackend
|
||||
from utils.callbacks import Callbacks
|
||||
from utils.datasets import create_dataloader
|
||||
from utils.general import (LOGGER, box_iou, check_dataset, check_img_size, check_requirements, check_suffix, check_yaml,
|
||||
from utils.general import (LOGGER, NCOLS, box_iou, check_dataset, check_img_size, check_requirements, check_yaml,
|
||||
coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
|
||||
scale_coords, xywh2xyxy, xyxy2xywh)
|
||||
from utils.metrics import ConfusionMatrix, ap_per_class
|
||||
@ -100,6 +100,7 @@ def run(data,
|
||||
name='exp', # save to project/name
|
||||
exist_ok=False, # existing project/name ok, do not increment
|
||||
half=True, # use FP16 half-precision inference
|
||||
dnn=False, # use OpenCV DNN for ONNX inference
|
||||
model=None,
|
||||
dataloader=None,
|
||||
save_dir=Path(''),
|
||||
@ -110,8 +111,10 @@ def run(data,
|
||||
# Initialize/load model and set device
|
||||
training = model is not None
|
||||
if training: # called by train.py
|
||||
device = next(model.parameters()).device # get model device
|
||||
device, pt = next(model.parameters()).device, True # get model device, PyTorch model
|
||||
|
||||
half &= device.type != 'cpu' # half precision only supported on CUDA
|
||||
model.half() if half else model.float()
|
||||
else: # called directly
|
||||
device = select_device(device, batch_size=batch_size)
|
||||
|
||||
@ -120,22 +123,21 @@ def run(data,
|
||||
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
|
||||
|
||||
# Load model
|
||||
check_suffix(weights, '.pt')
|
||||
model = attempt_load(weights, map_location=device) # load FP32 model
|
||||
gs = max(int(model.stride.max()), 32) # grid size (max stride)
|
||||
imgsz = check_img_size(imgsz, s=gs) # check image size
|
||||
|
||||
# Multi-GPU disabled, incompatible with .half() https://github.com/ultralytics/yolov5/issues/99
|
||||
# if device.type != 'cpu' and torch.cuda.device_count() > 1:
|
||||
# model = nn.DataParallel(model)
|
||||
model = DetectMultiBackend(weights, device=device, dnn=dnn)
|
||||
stride, pt = model.stride, model.pt
|
||||
imgsz = check_img_size(imgsz, s=stride) # check image size
|
||||
half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
|
||||
if pt:
|
||||
model.model.half() if half else model.model.float()
|
||||
else:
|
||||
half = False
|
||||
batch_size = 1 # export.py models default to batch-size 1
|
||||
device = torch.device('cpu')
|
||||
LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')
|
||||
|
||||
# Data
|
||||
data = check_dataset(data) # check
|
||||
|
||||
# Half
|
||||
half &= device.type != 'cpu' # half precision only supported on CUDA
|
||||
model.half() if half else model.float()
|
||||
|
||||
# Configure
|
||||
model.eval()
|
||||
is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # COCO dataset
|
||||
@ -145,11 +147,11 @@ def run(data,
|
||||
|
||||
# Dataloader
|
||||
if not training:
|
||||
if device.type != 'cpu':
|
||||
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once
|
||||
if pt and device.type != 'cpu':
|
||||
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # warmup
|
||||
pad = 0.0 if task == 'speed' else 0.5
|
||||
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
|
||||
dataloader = create_dataloader(data[task], imgsz, batch_size, gs, single_cls, pad=pad, rect=True,
|
||||
dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
|
||||
prefix=colorstr(f'{task}: '))[0]
|
||||
|
||||
seen = 0
|
||||
@ -160,32 +162,34 @@ def run(data,
|
||||
dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
||||
loss = torch.zeros(3, device=device)
|
||||
jdict, stats, ap, ap_class = [], [], [], []
|
||||
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
|
||||
pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
|
||||
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
|
||||
t1 = time_sync()
|
||||
img = img.to(device, non_blocking=True)
|
||||
img = img.half() if half else img.float() # uint8 to fp16/32
|
||||
img /= 255 # 0 - 255 to 0.0 - 1.0
|
||||
targets = targets.to(device)
|
||||
nb, _, height, width = img.shape # batch size, channels, height, width
|
||||
if pt:
|
||||
im = im.to(device, non_blocking=True)
|
||||
targets = targets.to(device)
|
||||
im = im.half() if half else im.float() # uint8 to fp16/32
|
||||
im /= 255 # 0 - 255 to 0.0 - 1.0
|
||||
nb, _, height, width = im.shape # batch size, channels, height, width
|
||||
t2 = time_sync()
|
||||
dt[0] += t2 - t1
|
||||
|
||||
# Run model
|
||||
out, train_out = model(img, augment=augment) # inference and training outputs
|
||||
# Inference
|
||||
out, train_out = model(im) if training else model(im, augment=augment, val=True) # inference, loss outputs
|
||||
dt[1] += time_sync() - t2
|
||||
|
||||
# Compute loss
|
||||
# Loss
|
||||
if compute_loss:
|
||||
loss += compute_loss([x.float() for x in train_out], targets)[1] # box, obj, cls
|
||||
|
||||
# Run NMS
|
||||
# NMS
|
||||
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
|
||||
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
|
||||
t3 = time_sync()
|
||||
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
|
||||
dt[2] += time_sync() - t3
|
||||
|
||||
# Statistics per image
|
||||
# Metrics
|
||||
for si, pred in enumerate(out):
|
||||
labels = targets[targets[:, 0] == si, 1:]
|
||||
nl = len(labels)
|
||||
@ -202,12 +206,12 @@ def run(data,
|
||||
if single_cls:
|
||||
pred[:, 5] = 0
|
||||
predn = pred.clone()
|
||||
scale_coords(img[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
|
||||
scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
|
||||
|
||||
# Evaluate
|
||||
if nl:
|
||||
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
|
||||
scale_coords(img[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
|
||||
scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
|
||||
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
|
||||
correct = process_batch(predn, labelsn, iouv)
|
||||
if plots:
|
||||
@ -221,16 +225,16 @@ def run(data,
|
||||
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
|
||||
if save_json:
|
||||
save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
|
||||
callbacks.run('on_val_image_end', pred, predn, path, names, img[si])
|
||||
callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
|
||||
|
||||
# Plot images
|
||||
if plots and batch_i < 3:
|
||||
f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels
|
||||
Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start()
|
||||
Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
|
||||
f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions
|
||||
Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start()
|
||||
Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
|
||||
|
||||
# Compute statistics
|
||||
# Compute metrics
|
||||
stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy
|
||||
if len(stats) and stats[0].any():
|
||||
p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
|
||||
@ -318,6 +322,7 @@ def parse_opt():
|
||||
parser.add_argument('--name', default='exp', help='save to project/name')
|
||||
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
|
||||
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
|
||||
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
|
||||
opt = parser.parse_args()
|
||||
opt.data = check_yaml(opt.data) # check YAML
|
||||
opt.save_json |= opt.data.endswith('coco.yaml')
|
||||
@ -330,28 +335,31 @@ def main(opt):
|
||||
check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
|
||||
|
||||
if opt.task in ('train', 'val', 'test'): # run normally
|
||||
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
|
||||
LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
|
||||
run(**vars(opt))
|
||||
|
||||
elif opt.task == 'speed': # speed benchmarks
|
||||
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
|
||||
for w in opt.weights if isinstance(opt.weights, list) else [opt.weights]:
|
||||
run(opt.data, weights=w, batch_size=opt.batch_size, imgsz=opt.imgsz, conf_thres=.25, iou_thres=.45,
|
||||
device=opt.device, save_json=False, plots=False)
|
||||
else:
|
||||
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
|
||||
opt.half = True # FP16 for fastest results
|
||||
if opt.task == 'speed': # speed benchmarks
|
||||
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
|
||||
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
|
||||
for opt.weights in weights:
|
||||
run(**vars(opt), plots=False)
|
||||
|
||||
elif opt.task == 'study': # run over a range of settings and save/plot
|
||||
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
|
||||
x = list(range(256, 1536 + 128, 128)) # x axis (image sizes)
|
||||
for w in opt.weights if isinstance(opt.weights, list) else [opt.weights]:
|
||||
f = f'study_{Path(opt.data).stem}_{Path(w).stem}.txt' # filename to save to
|
||||
y = [] # y axis
|
||||
for i in x: # img-size
|
||||
LOGGER.info(f'\nRunning {f} point {i}...')
|
||||
r, _, t = run(opt.data, weights=w, batch_size=opt.batch_size, imgsz=i, conf_thres=opt.conf_thres,
|
||||
iou_thres=opt.iou_thres, device=opt.device, save_json=opt.save_json, plots=False)
|
||||
y.append(r + t) # results and times
|
||||
np.savetxt(f, y, fmt='%10.4g') # save
|
||||
os.system('zip -r study.zip study_*.txt')
|
||||
plot_val_study(x=x) # plot
|
||||
elif opt.task == 'study': # speed vs mAP benchmarks
|
||||
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
|
||||
for opt.weights in weights:
|
||||
f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to
|
||||
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
|
||||
for opt.imgsz in x: # img-size
|
||||
LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
|
||||
r, _, t = run(**vars(opt), plots=False)
|
||||
y.append(r + t) # results and times
|
||||
np.savetxt(f, y, fmt='%10.4g') # save
|
||||
os.system('zip -r study.zip study_*.txt')
|
||||
plot_val_study(x=x) # plot
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
Loading…
x
Reference in New Issue
Block a user