YOLOv5 v5.0 release compatibility update for YOLOv3 (#1737)
* YOLOv5 v5.0 release compatibility update * Update README * Update README * Conv act LeakyReLU(0.1) * update plots_study() * update speeds
This commit is contained in:
+12
-7
@@ -37,17 +37,21 @@ def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
||||
bpr = (best > 1. / thr).float().mean() # best possible recall
|
||||
return bpr, aat
|
||||
|
||||
bpr, aat = metric(m.anchor_grid.clone().cpu().view(-1, 2))
|
||||
anchors = m.anchor_grid.clone().cpu().view(-1, 2) # current anchors
|
||||
bpr, aat = metric(anchors)
|
||||
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...')
|
||||
na = m.anchor_grid.numel() // 2 # number of anchors
|
||||
new_anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
||||
new_bpr = metric(new_anchors.reshape(-1, 2))[0]
|
||||
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}')
|
||||
new_bpr = metric(anchors)[0]
|
||||
if new_bpr > bpr: # replace anchors
|
||||
new_anchors = torch.tensor(new_anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchor_grid[:] = new_anchors.clone().view_as(m.anchor_grid) # for inference
|
||||
m.anchors[:] = new_anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
||||
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
|
||||
m.anchor_grid[:] = anchors.clone().view_as(m.anchor_grid) # for inference
|
||||
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.')
|
||||
else:
|
||||
@@ -98,7 +102,7 @@ def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=10
|
||||
|
||||
if isinstance(path, str): # *.yaml file
|
||||
with open(path) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.FullLoader) # model dict
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # model dict
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
||||
else:
|
||||
@@ -119,6 +123,7 @@ def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=10
|
||||
print(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, print(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
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# AWS EC2 instance startup 'MIME' script https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/
|
||||
# This script will run on every instance restart, not only on first start
|
||||
# --- DO NOT COPY ABOVE COMMENTS WHEN PASTING INTO USERDATA ---
|
||||
|
||||
Content-Type: multipart/mixed; boundary="//"
|
||||
MIME-Version: 1.0
|
||||
|
||||
--//
|
||||
Content-Type: text/cloud-config; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="cloud-config.txt"
|
||||
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
|
||||
--//
|
||||
Content-Type: text/x-shellscript; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment; filename="userdata.txt"
|
||||
|
||||
#!/bin/bash
|
||||
# --- paste contents of userdata.sh here ---
|
||||
--//
|
||||
@@ -0,0 +1,37 @@
|
||||
# Resume all interrupted trainings in yolov5/ dir including DDP trainings
|
||||
# Usage: $ python utils/aws/resume.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
sys.path.append('./') # to run '$ python *.py' files in subdirectories
|
||||
|
||||
port = 0 # --master_port
|
||||
path = Path('').resolve()
|
||||
for last in path.rglob('*/**/last.pt'):
|
||||
ckpt = torch.load(last)
|
||||
if ckpt['optimizer'] is None:
|
||||
continue
|
||||
|
||||
# Load opt.yaml
|
||||
with open(last.parent.parent / 'opt.yaml') as f:
|
||||
opt = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
# Get device count
|
||||
d = opt['device'].split(',') # devices
|
||||
nd = len(d) # number of devices
|
||||
ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel
|
||||
|
||||
if ddp: # multi-GPU
|
||||
port += 1
|
||||
cmd = f'python -m torch.distributed.launch --nproc_per_node {nd} --master_port {port} train.py --resume {last}'
|
||||
else: # single-GPU
|
||||
cmd = f'python train.py --resume {last}'
|
||||
|
||||
cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# AWS EC2 instance startup script https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
||||
# This script will run only once on first instance start (for a re-start script see mime.sh)
|
||||
# /home/ubuntu (ubuntu) or /home/ec2-user (amazon-linux) is working dir
|
||||
# Use >300 GB SSD
|
||||
|
||||
cd home/ubuntu
|
||||
if [ ! -d yolov5 ]; then
|
||||
echo "Running first-time script." # install dependencies, download COCO, pull Docker
|
||||
git clone https://github.com/ultralytics/yolov5 && sudo chmod -R 777 yolov5
|
||||
cd yolov5
|
||||
bash data/scripts/get_coco.sh && echo "Data done." &
|
||||
sudo docker pull ultralytics/yolov5:latest && echo "Docker done." &
|
||||
python -m pip install --upgrade pip && pip install -r requirements.txt && python detect.py && echo "Requirements done." &
|
||||
wait && echo "All tasks done." # finish background tasks
|
||||
else
|
||||
echo "Running re-start script." # resume interrupted runs
|
||||
i=0
|
||||
list=$(sudo docker ps -qa) # container list i.e. $'one\ntwo\nthree\nfour'
|
||||
while IFS= read -r id; do
|
||||
((i++))
|
||||
echo "restarting container $i: $id"
|
||||
sudo docker start $id
|
||||
# sudo docker exec -it $id python train.py --resume # single-GPU
|
||||
sudo docker exec -d $id python utils/aws/resume.py # multi-scenario
|
||||
done <<<"$list"
|
||||
fi
|
||||
+141
-111
@@ -20,12 +20,13 @@ from PIL import Image, ExifTags
|
||||
from torch.utils.data import Dataset
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.general import xyxy2xywh, xywh2xyxy, clean_str
|
||||
from utils.general import check_requirements, xyxy2xywh, xywh2xyxy, xywhn2xyxy, xyn2xy, segment2box, segments2boxes, \
|
||||
resample_segments, clean_str
|
||||
from utils.torch_utils import torch_distributed_zero_first
|
||||
|
||||
# Parameters
|
||||
help_url = 'https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data'
|
||||
img_formats = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng'] # acceptable image suffixes
|
||||
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
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -119,9 +120,8 @@ class _RepeatSampler(object):
|
||||
|
||||
|
||||
class LoadImages: # for inference
|
||||
def __init__(self, path, img_size=640):
|
||||
p = str(Path(path)) # os-agnostic
|
||||
p = os.path.abspath(p) # absolute path
|
||||
def __init__(self, path, img_size=640, stride=32):
|
||||
p = str(Path(path).absolute()) # os-agnostic absolute path
|
||||
if '*' in p:
|
||||
files = sorted(glob.glob(p, recursive=True)) # glob
|
||||
elif os.path.isdir(p):
|
||||
@@ -136,6 +136,7 @@ class LoadImages: # for inference
|
||||
ni, nv = len(images), len(videos)
|
||||
|
||||
self.img_size = img_size
|
||||
self.stride = stride
|
||||
self.files = images + videos
|
||||
self.nf = ni + nv # number of files
|
||||
self.video_flag = [False] * ni + [True] * nv
|
||||
@@ -181,7 +182,7 @@ class LoadImages: # for inference
|
||||
print(f'image {self.count}/{self.nf} {path}: ', end='')
|
||||
|
||||
# Padded resize
|
||||
img = letterbox(img0, new_shape=self.img_size)[0]
|
||||
img = letterbox(img0, self.img_size, stride=self.stride)[0]
|
||||
|
||||
# Convert
|
||||
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
@@ -199,8 +200,9 @@ class LoadImages: # for inference
|
||||
|
||||
|
||||
class LoadWebcam: # for inference
|
||||
def __init__(self, pipe='0', img_size=640):
|
||||
def __init__(self, pipe='0', img_size=640, stride=32):
|
||||
self.img_size = img_size
|
||||
self.stride = stride
|
||||
|
||||
if pipe.isnumeric():
|
||||
pipe = eval(pipe) # local camera
|
||||
@@ -243,7 +245,7 @@ class LoadWebcam: # for inference
|
||||
print(f'webcam {self.count}: ', end='')
|
||||
|
||||
# Padded resize
|
||||
img = letterbox(img0, new_shape=self.img_size)[0]
|
||||
img = letterbox(img0, self.img_size, stride=self.stride)[0]
|
||||
|
||||
# Convert
|
||||
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
||||
@@ -256,9 +258,10 @@ class LoadWebcam: # for inference
|
||||
|
||||
|
||||
class LoadStreams: # multiple IP or RTSP cameras
|
||||
def __init__(self, sources='streams.txt', img_size=640):
|
||||
def __init__(self, sources='streams.txt', img_size=640, stride=32):
|
||||
self.mode = 'stream'
|
||||
self.img_size = img_size
|
||||
self.stride = stride
|
||||
|
||||
if os.path.isfile(sources):
|
||||
with open(sources, 'r') as f:
|
||||
@@ -272,19 +275,25 @@ class LoadStreams: # multiple IP or RTSP cameras
|
||||
for i, s in enumerate(sources):
|
||||
# Start the thread to read frames from the video stream
|
||||
print(f'{i + 1}/{n}: {s}... ', end='')
|
||||
cap = cv2.VideoCapture(eval(s) if s.isnumeric() else s)
|
||||
url = eval(s) if s.isnumeric() else s
|
||||
if 'youtube.com/' in url or 'youtu.be/' in url: # if source is YouTube video
|
||||
check_requirements(('pafy', 'youtube_dl'))
|
||||
import pafy
|
||||
url = pafy.new(url).getbest(preftype="mp4").url
|
||||
cap = cv2.VideoCapture(url)
|
||||
assert cap.isOpened(), f'Failed to open {s}'
|
||||
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS) % 100
|
||||
self.fps = cap.get(cv2.CAP_PROP_FPS) % 100
|
||||
|
||||
_, self.imgs[i] = cap.read() # guarantee first frame
|
||||
thread = Thread(target=self.update, args=([i, cap]), daemon=True)
|
||||
print(f' success ({w}x{h} at {fps:.2f} FPS).')
|
||||
print(f' success ({w}x{h} at {self.fps:.2f} FPS).')
|
||||
thread.start()
|
||||
print('') # newline
|
||||
|
||||
# check for common shapes
|
||||
s = np.stack([letterbox(x, new_shape=self.img_size)[0].shape for x in self.imgs], 0) # inference shapes
|
||||
s = np.stack([letterbox(x, self.img_size, stride=self.stride)[0].shape for x in self.imgs], 0) # shapes
|
||||
self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal
|
||||
if not self.rect:
|
||||
print('WARNING: Different stream shapes detected. For optimal performance supply similarly-shaped streams.')
|
||||
@@ -297,9 +306,10 @@ class LoadStreams: # multiple IP or RTSP cameras
|
||||
# _, self.imgs[index] = cap.read()
|
||||
cap.grab()
|
||||
if n == 4: # read every 4th frame
|
||||
_, self.imgs[index] = cap.retrieve()
|
||||
success, im = cap.retrieve()
|
||||
self.imgs[index] = im if success else self.imgs[index] * 0
|
||||
n = 0
|
||||
time.sleep(0.01) # wait time
|
||||
time.sleep(1 / self.fps) # wait time
|
||||
|
||||
def __iter__(self):
|
||||
self.count = -1
|
||||
@@ -313,7 +323,7 @@ class LoadStreams: # multiple IP or RTSP cameras
|
||||
raise StopIteration
|
||||
|
||||
# Letterbox
|
||||
img = [letterbox(x, new_shape=self.img_size, auto=self.rect)[0] for x in img0]
|
||||
img = [letterbox(x, self.img_size, auto=self.rect, stride=self.stride)[0] for x in img0]
|
||||
|
||||
# Stack
|
||||
img = np.stack(img, 0)
|
||||
@@ -331,7 +341,7 @@ class LoadStreams: # multiple IP or RTSP cameras
|
||||
def img2label_paths(img_paths):
|
||||
# Define label paths as a function of image paths
|
||||
sa, sb = os.sep + 'images' + os.sep, os.sep + 'labels' + os.sep # /images/, /labels/ substrings
|
||||
return [x.replace(sa, sb, 1).replace('.' + x.split('.')[-1], '.txt') for x in img_paths]
|
||||
return ['txt'.join(x.replace(sa, sb, 1).rsplit(x.split('.')[-1], 1)) for x in img_paths]
|
||||
|
||||
|
||||
class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
@@ -345,6 +355,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
self.mosaic = self.augment and not self.rect # load 4 images at a time into a mosaic (only during training)
|
||||
self.mosaic_border = [-img_size // 2, -img_size // 2]
|
||||
self.stride = stride
|
||||
self.path = path
|
||||
|
||||
try:
|
||||
f = [] # image files
|
||||
@@ -352,37 +363,42 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
p = Path(p) # os-agnostic
|
||||
if p.is_dir(): # dir
|
||||
f += glob.glob(str(p / '**' / '*.*'), recursive=True)
|
||||
# f = list(p.rglob('**/*.*')) # pathlib
|
||||
elif p.is_file(): # file
|
||||
with open(p, 'r') as t:
|
||||
t = t.read().strip().splitlines()
|
||||
parent = str(p.parent) + os.sep
|
||||
f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path
|
||||
# f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
|
||||
else:
|
||||
raise Exception(f'{prefix}{p} does not exist')
|
||||
self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in img_formats])
|
||||
# self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib
|
||||
assert self.img_files, f'{prefix}No images found'
|
||||
except Exception as e:
|
||||
raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {help_url}')
|
||||
|
||||
# Check cache
|
||||
self.label_files = img2label_paths(self.img_files) # labels
|
||||
cache_path = Path(self.label_files[0]).parent.with_suffix('.cache') # cached labels
|
||||
cache_path = (p if p.is_file() else Path(self.label_files[0]).parent).with_suffix('.cache') # cached labels
|
||||
if cache_path.is_file():
|
||||
cache = torch.load(cache_path) # load
|
||||
if cache['hash'] != get_hash(self.label_files + self.img_files) or 'results' not in cache: # changed
|
||||
cache = self.cache_labels(cache_path, prefix) # re-cache
|
||||
cache, exists = torch.load(cache_path), True # load
|
||||
if cache['hash'] != get_hash(self.label_files + self.img_files) or 'version' not in cache: # changed
|
||||
cache, exists = self.cache_labels(cache_path, prefix), False # re-cache
|
||||
else:
|
||||
cache = self.cache_labels(cache_path, prefix) # cache
|
||||
cache, exists = self.cache_labels(cache_path, prefix), False # cache
|
||||
|
||||
# Display cache
|
||||
[nf, nm, ne, nc, n] = cache.pop('results') # found, missing, empty, corrupted, total
|
||||
desc = f"Scanning '{cache_path}' for images and labels... {nf} found, {nm} missing, {ne} empty, {nc} corrupted"
|
||||
tqdm(None, desc=prefix + desc, total=n, initial=n)
|
||||
nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupted, total
|
||||
if exists:
|
||||
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
|
||||
assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {help_url}'
|
||||
|
||||
# Read cache
|
||||
cache.pop('hash') # remove hash
|
||||
labels, shapes = zip(*cache.values())
|
||||
cache.pop('version') # remove version
|
||||
labels, shapes, self.segments = zip(*cache.values())
|
||||
self.labels = list(labels)
|
||||
self.shapes = np.array(shapes, dtype=np.float64)
|
||||
self.img_files = list(cache.keys()) # update
|
||||
@@ -433,6 +449,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # img, hw_original, hw_resized = load_image(self, i)
|
||||
gb += self.imgs[i].nbytes
|
||||
pbar.desc = f'{prefix}Caching images ({gb / 1E9:.1f}GB)'
|
||||
pbar.close()
|
||||
|
||||
def cache_labels(self, path=Path('./labels.cache'), prefix=''):
|
||||
# Cache dataset labels, check images and read shapes
|
||||
@@ -445,13 +462,20 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
im = Image.open(im_file)
|
||||
im.verify() # PIL verify
|
||||
shape = exif_size(im) # image size
|
||||
assert (shape[0] > 9) & (shape[1] > 9), 'image size <10 pixels'
|
||||
segments = [] # instance segments
|
||||
assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels'
|
||||
assert im.format.lower() in img_formats, f'invalid image format {im.format}'
|
||||
|
||||
# verify labels
|
||||
if os.path.isfile(lb_file):
|
||||
nf += 1 # label found
|
||||
with open(lb_file, 'r') as f:
|
||||
l = np.array([x.split() for x in f.read().strip().splitlines()], dtype=np.float32) # labels
|
||||
l = [x.split() for x in f.read().strip().splitlines()]
|
||||
if any([len(x) > 8 for x in l]): # is segment
|
||||
classes = np.array([x[0] for x in l], dtype=np.float32)
|
||||
segments = [np.array(x[1:], dtype=np.float32).reshape(-1, 2) for x in l] # (cls, xy1...)
|
||||
l = np.concatenate((classes.reshape(-1, 1), segments2boxes(segments)), 1) # (cls, xywh)
|
||||
l = np.array(l, dtype=np.float32)
|
||||
if len(l):
|
||||
assert l.shape[1] == 5, 'labels require 5 columns each'
|
||||
assert (l >= 0).all(), 'negative labels'
|
||||
@@ -463,19 +487,21 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
else:
|
||||
nm += 1 # label missing
|
||||
l = np.zeros((0, 5), dtype=np.float32)
|
||||
x[im_file] = [l, shape]
|
||||
x[im_file] = [l, shape, segments]
|
||||
except Exception as e:
|
||||
nc += 1
|
||||
print(f'{prefix}WARNING: Ignoring corrupted image and/or label {im_file}: {e}')
|
||||
|
||||
pbar.desc = f"{prefix}Scanning '{path.parent / path.stem}' for images and labels... " \
|
||||
pbar.desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels... " \
|
||||
f"{nf} found, {nm} missing, {ne} empty, {nc} corrupted"
|
||||
pbar.close()
|
||||
|
||||
if nf == 0:
|
||||
print(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, i + 1]
|
||||
x['results'] = nf, nm, ne, nc, i + 1
|
||||
x['version'] = 0.1 # cache version
|
||||
torch.save(x, path) # save for next time
|
||||
logging.info(f'{prefix}New cache created: {path}')
|
||||
return x
|
||||
@@ -515,16 +541,9 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
||||
img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment)
|
||||
shapes = (h0, w0), ((h / h0, w / w0), pad) # for COCO mAP rescaling
|
||||
|
||||
# Load labels
|
||||
labels = []
|
||||
x = self.labels[index]
|
||||
if x.size > 0:
|
||||
# Normalized xywh to pixel xyxy format
|
||||
labels = x.copy()
|
||||
labels[:, 1] = ratio[0] * w * (x[:, 1] - x[:, 3] / 2) + pad[0] # pad width
|
||||
labels[:, 2] = ratio[1] * h * (x[:, 2] - x[:, 4] / 2) + pad[1] # pad height
|
||||
labels[:, 3] = ratio[0] * w * (x[:, 1] + x[:, 3] / 2) + pad[0]
|
||||
labels[:, 4] = ratio[1] * h * (x[:, 2] + x[:, 4] / 2) + pad[1]
|
||||
labels = self.labels[index].copy()
|
||||
if labels.size: # normalized xywh to pixel xyxy format
|
||||
labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1])
|
||||
|
||||
if self.augment:
|
||||
# Augment imagespace
|
||||
@@ -637,19 +656,25 @@ def augment_hsv(img, hgain=0.5, sgain=0.5, vgain=0.5):
|
||||
img_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))).astype(dtype)
|
||||
cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR, dst=img) # no return needed
|
||||
|
||||
# Histogram equalization
|
||||
# if random.random() < 0.2:
|
||||
# for i in range(3):
|
||||
# img[:, :, i] = cv2.equalizeHist(img[:, :, i])
|
||||
|
||||
def hist_equalize(img, clahe=True, bgr=False):
|
||||
# Equalize histogram on BGR image 'img' with img.shape(n,m,3) and range 0-255
|
||||
yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV if bgr else cv2.COLOR_RGB2YUV)
|
||||
if clahe:
|
||||
c = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||
yuv[:, :, 0] = c.apply(yuv[:, :, 0])
|
||||
else:
|
||||
yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0]) # equalize Y channel histogram
|
||||
return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR if bgr else cv2.COLOR_YUV2RGB) # convert YUV image to RGB
|
||||
|
||||
|
||||
def load_mosaic(self, index):
|
||||
# loads images in a 4-mosaic
|
||||
|
||||
labels4 = []
|
||||
labels4, segments4 = [], []
|
||||
s = self.img_size
|
||||
yc, xc = [int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border] # mosaic center x, y
|
||||
indices = [index] + [self.indices[random.randint(0, self.n - 1)] for _ in range(3)] # 3 additional image indices
|
||||
indices = [index] + random.choices(self.indices, k=3) # 3 additional image indices
|
||||
for i, index in enumerate(indices):
|
||||
# Load image
|
||||
img, _, (h, w) = load_image(self, index)
|
||||
@@ -674,23 +699,21 @@ def load_mosaic(self, index):
|
||||
padh = y1a - y1b
|
||||
|
||||
# Labels
|
||||
x = self.labels[index]
|
||||
labels = x.copy()
|
||||
if x.size > 0: # Normalized xywh to pixel xyxy format
|
||||
labels[:, 1] = w * (x[:, 1] - x[:, 3] / 2) + padw
|
||||
labels[:, 2] = h * (x[:, 2] - x[:, 4] / 2) + padh
|
||||
labels[:, 3] = w * (x[:, 1] + x[:, 3] / 2) + padw
|
||||
labels[:, 4] = h * (x[:, 2] + x[:, 4] / 2) + padh
|
||||
labels, segments = self.labels[index].copy(), self.segments[index].copy()
|
||||
if labels.size:
|
||||
labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padw, padh) # normalized xywh to pixel xyxy format
|
||||
segments = [xyn2xy(x, w, h, padw, padh) for x in segments]
|
||||
labels4.append(labels)
|
||||
segments4.extend(segments)
|
||||
|
||||
# Concat/clip labels
|
||||
if len(labels4):
|
||||
labels4 = np.concatenate(labels4, 0)
|
||||
np.clip(labels4[:, 1:], 0, 2 * s, out=labels4[:, 1:]) # use with random_perspective
|
||||
# img4, labels4 = replicate(img4, labels4) # replicate
|
||||
labels4 = np.concatenate(labels4, 0)
|
||||
for x in (labels4[:, 1:], *segments4):
|
||||
np.clip(x, 0, 2 * s, out=x) # clip when using random_perspective()
|
||||
# img4, labels4 = replicate(img4, labels4) # replicate
|
||||
|
||||
# Augment
|
||||
img4, labels4 = random_perspective(img4, labels4,
|
||||
img4, labels4 = random_perspective(img4, labels4, segments4,
|
||||
degrees=self.hyp['degrees'],
|
||||
translate=self.hyp['translate'],
|
||||
scale=self.hyp['scale'],
|
||||
@@ -704,9 +727,9 @@ def load_mosaic(self, index):
|
||||
def load_mosaic9(self, index):
|
||||
# loads images in a 9-mosaic
|
||||
|
||||
labels9 = []
|
||||
labels9, segments9 = [], []
|
||||
s = self.img_size
|
||||
indices = [index] + [self.indices[random.randint(0, self.n - 1)] for _ in range(8)] # 8 additional image indices
|
||||
indices = [index] + random.choices(self.indices, k=8) # 8 additional image indices
|
||||
for i, index in enumerate(indices):
|
||||
# Load image
|
||||
img, _, (h, w) = load_image(self, index)
|
||||
@@ -737,34 +760,34 @@ def load_mosaic9(self, index):
|
||||
x1, y1, x2, y2 = [max(x, 0) for x in c] # allocate coords
|
||||
|
||||
# Labels
|
||||
x = self.labels[index]
|
||||
labels = x.copy()
|
||||
if x.size > 0: # Normalized xywh to pixel xyxy format
|
||||
labels[:, 1] = w * (x[:, 1] - x[:, 3] / 2) + padx
|
||||
labels[:, 2] = h * (x[:, 2] - x[:, 4] / 2) + pady
|
||||
labels[:, 3] = w * (x[:, 1] + x[:, 3] / 2) + padx
|
||||
labels[:, 4] = h * (x[:, 2] + x[:, 4] / 2) + pady
|
||||
labels, segments = self.labels[index].copy(), self.segments[index].copy()
|
||||
if labels.size:
|
||||
labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padx, pady) # normalized xywh to pixel xyxy format
|
||||
segments = [xyn2xy(x, w, h, padx, pady) for x in segments]
|
||||
labels9.append(labels)
|
||||
segments9.extend(segments)
|
||||
|
||||
# Image
|
||||
img9[y1:y2, x1:x2] = img[y1 - pady:, x1 - padx:] # img9[ymin:ymax, xmin:xmax]
|
||||
hp, wp = h, w # height, width previous
|
||||
|
||||
# Offset
|
||||
yc, xc = [int(random.uniform(0, s)) for x in self.mosaic_border] # mosaic center x, y
|
||||
yc, xc = [int(random.uniform(0, s)) for _ in self.mosaic_border] # mosaic center x, y
|
||||
img9 = img9[yc:yc + 2 * s, xc:xc + 2 * s]
|
||||
|
||||
# Concat/clip labels
|
||||
if len(labels9):
|
||||
labels9 = np.concatenate(labels9, 0)
|
||||
labels9[:, [1, 3]] -= xc
|
||||
labels9[:, [2, 4]] -= yc
|
||||
labels9 = np.concatenate(labels9, 0)
|
||||
labels9[:, [1, 3]] -= xc
|
||||
labels9[:, [2, 4]] -= yc
|
||||
c = np.array([xc, yc]) # centers
|
||||
segments9 = [x - c for x in segments9]
|
||||
|
||||
np.clip(labels9[:, 1:], 0, 2 * s, out=labels9[:, 1:]) # use with random_perspective
|
||||
# img9, labels9 = replicate(img9, labels9) # replicate
|
||||
for x in (labels9[:, 1:], *segments9):
|
||||
np.clip(x, 0, 2 * s, out=x) # clip when using random_perspective()
|
||||
# img9, labels9 = replicate(img9, labels9) # replicate
|
||||
|
||||
# Augment
|
||||
img9, labels9 = random_perspective(img9, labels9,
|
||||
img9, labels9 = random_perspective(img9, labels9, segments9,
|
||||
degrees=self.hyp['degrees'],
|
||||
translate=self.hyp['translate'],
|
||||
scale=self.hyp['scale'],
|
||||
@@ -792,8 +815,8 @@ def replicate(img, labels):
|
||||
return img, labels
|
||||
|
||||
|
||||
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True):
|
||||
# Resize image to a 32-pixel-multiple rectangle https://github.com/ultralytics/yolov3/issues/232
|
||||
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
|
||||
# Resize and pad image while meeting stride-multiple constraints
|
||||
shape = img.shape[:2] # current shape [height, width]
|
||||
if isinstance(new_shape, int):
|
||||
new_shape = (new_shape, new_shape)
|
||||
@@ -808,7 +831,7 @@ def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scale
|
||||
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
|
||||
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
|
||||
if auto: # minimum rectangle
|
||||
dw, dh = np.mod(dw, 32), np.mod(dh, 32) # wh padding
|
||||
dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding
|
||||
elif scaleFill: # stretch
|
||||
dw, dh = 0.0, 0.0
|
||||
new_unpad = (new_shape[1], new_shape[0])
|
||||
@@ -825,7 +848,8 @@ def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scale
|
||||
return img, ratio, (dw, dh)
|
||||
|
||||
|
||||
def random_perspective(img, targets=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0, border=(0, 0)):
|
||||
def random_perspective(img, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0,
|
||||
border=(0, 0)):
|
||||
# torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10))
|
||||
# targets = [cls, xyxy]
|
||||
|
||||
@@ -877,37 +901,38 @@ def random_perspective(img, targets=(), degrees=10, translate=.1, scale=.1, shea
|
||||
# Transform label coordinates
|
||||
n = len(targets)
|
||||
if n:
|
||||
# warp points
|
||||
xy = np.ones((n * 4, 3))
|
||||
xy[:, :2] = targets[:, [1, 2, 3, 4, 1, 4, 3, 2]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1
|
||||
xy = xy @ M.T # transform
|
||||
if perspective:
|
||||
xy = (xy[:, :2] / xy[:, 2:3]).reshape(n, 8) # rescale
|
||||
else: # affine
|
||||
xy = xy[:, :2].reshape(n, 8)
|
||||
use_segments = any(x.any() for x in segments)
|
||||
new = np.zeros((n, 4))
|
||||
if use_segments: # warp segments
|
||||
segments = resample_segments(segments) # upsample
|
||||
for i, segment in enumerate(segments):
|
||||
xy = np.ones((len(segment), 3))
|
||||
xy[:, :2] = segment
|
||||
xy = xy @ M.T # transform
|
||||
xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine
|
||||
|
||||
# create new boxes
|
||||
x = xy[:, [0, 2, 4, 6]]
|
||||
y = xy[:, [1, 3, 5, 7]]
|
||||
xy = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T
|
||||
# clip
|
||||
new[i] = segment2box(xy, width, height)
|
||||
|
||||
# # apply angle-based reduction of bounding boxes
|
||||
# radians = a * math.pi / 180
|
||||
# reduction = max(abs(math.sin(radians)), abs(math.cos(radians))) ** 0.5
|
||||
# x = (xy[:, 2] + xy[:, 0]) / 2
|
||||
# y = (xy[:, 3] + xy[:, 1]) / 2
|
||||
# w = (xy[:, 2] - xy[:, 0]) * reduction
|
||||
# h = (xy[:, 3] - xy[:, 1]) * reduction
|
||||
# xy = np.concatenate((x - w / 2, y - h / 2, x + w / 2, y + h / 2)).reshape(4, n).T
|
||||
else: # warp boxes
|
||||
xy = np.ones((n * 4, 3))
|
||||
xy[:, :2] = targets[:, [1, 2, 3, 4, 1, 4, 3, 2]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1
|
||||
xy = xy @ M.T # transform
|
||||
xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine
|
||||
|
||||
# clip boxes
|
||||
xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width)
|
||||
xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height)
|
||||
# create new boxes
|
||||
x = xy[:, [0, 2, 4, 6]]
|
||||
y = xy[:, [1, 3, 5, 7]]
|
||||
new = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T
|
||||
|
||||
# clip
|
||||
new[:, [0, 2]] = new[:, [0, 2]].clip(0, width)
|
||||
new[:, [1, 3]] = new[:, [1, 3]].clip(0, height)
|
||||
|
||||
# filter candidates
|
||||
i = box_candidates(box1=targets[:, 1:5].T * s, box2=xy.T)
|
||||
i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10)
|
||||
targets = targets[i]
|
||||
targets[:, 1:5] = xy[i]
|
||||
targets[:, 1:5] = new[i]
|
||||
|
||||
return img, targets
|
||||
|
||||
@@ -1016,19 +1041,24 @@ def extract_boxes(path='../coco128/'): # from utils.datasets import *; extract_
|
||||
assert cv2.imwrite(str(f), im[b[1]:b[3], b[0]:b[2]]), f'box failure in {f}'
|
||||
|
||||
|
||||
def autosplit(path='../coco128', weights=(0.9, 0.1, 0.0)): # from utils.datasets import *; autosplit('../coco128')
|
||||
def autosplit(path='../coco128', weights=(0.9, 0.1, 0.0), annotated_only=False):
|
||||
""" Autosplit a dataset into train/val/test splits and save path/autosplit_*.txt files
|
||||
# Arguments
|
||||
path: Path to images directory
|
||||
weights: Train, val, test weights (list)
|
||||
Usage: from utils.datasets import *; autosplit('../coco128')
|
||||
Arguments
|
||||
path: Path to images directory
|
||||
weights: Train, val, test weights (list)
|
||||
annotated_only: Only use images with an annotated txt file
|
||||
"""
|
||||
path = Path(path) # images dir
|
||||
files = list(path.rglob('*.*'))
|
||||
files = sum([list(path.rglob(f"*.{img_ext}")) for img_ext in img_formats], []) # image files only
|
||||
n = len(files) # number of files
|
||||
indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split
|
||||
|
||||
txt = ['autosplit_train.txt', 'autosplit_val.txt', 'autosplit_test.txt'] # 3 txt files
|
||||
[(path / x).unlink() for x in txt if (path / x).exists()] # remove existing
|
||||
|
||||
print(f'Autosplitting images from {path}' + ', using *.txt labeled images only' * annotated_only)
|
||||
for i, img in tqdm(zip(indices, files), total=n):
|
||||
if img.suffix[1:] in img_formats:
|
||||
if not annotated_only or Path(img2label_paths([str(img)])[0]).exists(): # check label
|
||||
with open(path / txt[i], 'a') as f:
|
||||
f.write(str(img) + '\n') # add image to txt file
|
||||
|
||||
+131
-32
@@ -1,9 +1,10 @@
|
||||
# General utils
|
||||
# YOLOv3 general utils
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torchvision
|
||||
import yaml
|
||||
@@ -23,6 +25,7 @@ from utils.torch_utils import init_torch_seeds
|
||||
# Settings
|
||||
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
||||
np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
|
||||
pd.options.display.max_columns = 10
|
||||
cv2.setNumThreads(0) # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader)
|
||||
os.environ['NUMEXPR_MAX_THREADS'] = str(min(os.cpu_count(), 8)) # NumExpr max threads
|
||||
|
||||
@@ -46,40 +49,75 @@ def get_latest_run(search_dir='.'):
|
||||
return max(last_list, key=os.path.getctime) if last_list else ''
|
||||
|
||||
|
||||
def isdocker():
|
||||
# Is environment a Docker container
|
||||
return Path('/workspace').exists() # or Path('/.dockerenv').exists()
|
||||
|
||||
|
||||
def emojis(str=''):
|
||||
# Return platform-dependent emoji-safe version of string
|
||||
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
|
||||
|
||||
|
||||
def check_online():
|
||||
# Check internet connectivity
|
||||
import socket
|
||||
try:
|
||||
socket.create_connection(("1.1.1.1", 53)) # check host accesability
|
||||
socket.create_connection(("1.1.1.1", 443), 5) # check host accesability
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def check_git_status():
|
||||
# Suggest 'git pull' if YOLOv5 is out of date
|
||||
# Recommend 'git pull' if code is out of date
|
||||
print(colorstr('github: '), end='')
|
||||
try:
|
||||
if Path('.git').exists() and check_online():
|
||||
url = subprocess.check_output(
|
||||
'git fetch && git config --get remote.origin.url', shell=True).decode('utf-8')[:-1]
|
||||
n = int(subprocess.check_output(
|
||||
'git rev-list $(git rev-parse --abbrev-ref HEAD)..origin/master --count', shell=True)) # commits behind
|
||||
if n > 0:
|
||||
print(f"⚠️ WARNING: code is out of date by {n} {'commits' if n > 1 else 'commmit'}. "
|
||||
f"Use 'git pull' to update or 'git clone {url}' to download latest.")
|
||||
else:
|
||||
print(f'up to date with {url} ✅')
|
||||
assert Path('.git').exists(), 'skipping check (not a git repository)'
|
||||
assert not isdocker(), 'skipping check (Docker image)'
|
||||
assert check_online(), 'skipping check (offline)'
|
||||
|
||||
cmd = 'git fetch && git config --get remote.origin.url'
|
||||
url = subprocess.check_output(cmd, shell=True).decode().strip().rstrip('.git') # github repo url
|
||||
branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out
|
||||
n = int(subprocess.check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind
|
||||
if n > 0:
|
||||
s = f"⚠️ WARNING: code is out of date by {n} commit{'s' * (n > 1)}. " \
|
||||
f"Use 'git pull' to update or 'git clone {url}' to download latest."
|
||||
else:
|
||||
s = f'up to date with {url} ✅'
|
||||
print(emojis(s)) # emoji-safe
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def check_requirements(file='requirements.txt'):
|
||||
# Check installed dependencies meet requirements
|
||||
import pkg_resources
|
||||
requirements = pkg_resources.parse_requirements(Path(file).open())
|
||||
requirements = [x.name + ''.join(*x.specs) if len(x.specs) else x.name for x in requirements]
|
||||
pkg_resources.require(requirements) # DistributionNotFound or VersionConflict exception if requirements not met
|
||||
def check_requirements(requirements='requirements.txt', exclude=()):
|
||||
# Check installed dependencies meet requirements (pass *.txt file or list of packages)
|
||||
import pkg_resources as pkg
|
||||
prefix = colorstr('red', 'bold', 'requirements:')
|
||||
if isinstance(requirements, (str, Path)): # requirements.txt file
|
||||
file = Path(requirements)
|
||||
if not file.exists():
|
||||
print(f"{prefix} {file.resolve()} not found, check failed.")
|
||||
return
|
||||
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude]
|
||||
else: # list or tuple of packages
|
||||
requirements = [x for x in requirements if x not in exclude]
|
||||
|
||||
n = 0 # number of packages updates
|
||||
for r in requirements:
|
||||
try:
|
||||
pkg.require(r)
|
||||
except Exception as e: # DistributionNotFound or VersionConflict if requirements not met
|
||||
n += 1
|
||||
print(f"{prefix} {e.req} not found and is required by YOLOv3, attempting auto-update...")
|
||||
print(subprocess.check_output(f"pip install '{e.req}'", shell=True).decode())
|
||||
|
||||
if n: # if packages updated
|
||||
source = file.resolve() if 'file' in locals() else requirements
|
||||
s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
|
||||
f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
|
||||
print(emojis(s)) # emoji-safe
|
||||
|
||||
|
||||
def check_img_size(img_size, s=32):
|
||||
@@ -90,14 +128,28 @@ def check_img_size(img_size, s=32):
|
||||
return new_size
|
||||
|
||||
|
||||
def check_imshow():
|
||||
# Check if environment supports image displays
|
||||
try:
|
||||
assert not isdocker(), 'cv2.imshow() is disabled in Docker environments'
|
||||
cv2.imshow('test', np.zeros((1, 1, 3)))
|
||||
cv2.waitKey(1)
|
||||
cv2.destroyAllWindows()
|
||||
cv2.waitKey(1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'WARNING: Environment does not support cv2.imshow() or PIL Image.show() image displays\n{e}')
|
||||
return False
|
||||
|
||||
|
||||
def check_file(file):
|
||||
# Search for file if not found
|
||||
if os.path.isfile(file) or file == '':
|
||||
if Path(file).is_file() or file == '':
|
||||
return file
|
||||
else:
|
||||
files = glob.glob('./**/' + file, recursive=True) # find file
|
||||
assert len(files), 'File Not Found: %s' % file # assert file was found
|
||||
assert len(files) == 1, "Multiple files match '%s', specify exact path: %s" % (file, files) # assert unique
|
||||
assert len(files), f'File Not Found: {file}' # assert file was found
|
||||
assert len(files) == 1, f"Multiple files match '{file}', specify exact path: {files}" # assert unique
|
||||
return files[0] # return file
|
||||
|
||||
|
||||
@@ -220,6 +272,50 @@ def xywh2xyxy(x):
|
||||
return y
|
||||
|
||||
|
||||
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x
|
||||
y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y
|
||||
y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x
|
||||
y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y
|
||||
return y
|
||||
|
||||
|
||||
def xyn2xy(x, w=640, h=640, padw=0, padh=0):
|
||||
# Convert normalized segments into pixel segments, shape (n,2)
|
||||
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
|
||||
y[:, 0] = w * x[:, 0] + padw # top left x
|
||||
y[:, 1] = h * x[:, 1] + padh # top left y
|
||||
return y
|
||||
|
||||
|
||||
def segment2box(segment, width=640, height=640):
|
||||
# Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy)
|
||||
x, y = segment.T # segment xy
|
||||
inside = (x >= 0) & (y >= 0) & (x <= width) & (y <= height)
|
||||
x, y, = x[inside], y[inside]
|
||||
return np.array([x.min(), y.min(), x.max(), y.max()]) if any(x) else np.zeros((1, 4)) # xyxy
|
||||
|
||||
|
||||
def segments2boxes(segments):
|
||||
# Convert segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh)
|
||||
boxes = []
|
||||
for s in segments:
|
||||
x, y = s.T # segment xy
|
||||
boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy
|
||||
return xyxy2xywh(np.array(boxes)) # cls, xywh
|
||||
|
||||
|
||||
def resample_segments(segments, n=1000):
|
||||
# Up-sample an (n,2) segment
|
||||
for i, s in enumerate(segments):
|
||||
x = np.linspace(0, len(s) - 1, n)
|
||||
xp = np.arange(len(s))
|
||||
segments[i] = np.concatenate([np.interp(x, xp, s[:, i]) for i in range(2)]).reshape(2, -1).T # segment xy
|
||||
return segments
|
||||
|
||||
|
||||
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
|
||||
# Rescale coords (xyxy) from img1_shape to img0_shape
|
||||
if ratio_pad is None: # calculate from img0_shape
|
||||
@@ -244,7 +340,7 @@ def clip_coords(boxes, img_shape):
|
||||
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
||||
|
||||
|
||||
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
|
||||
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
|
||||
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
||||
box2 = box2.T
|
||||
|
||||
@@ -280,7 +376,7 @@ def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=
|
||||
elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
|
||||
v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
|
||||
with torch.no_grad():
|
||||
alpha = v / ((1 + eps) - iou + v)
|
||||
alpha = v / (v - iou + (1 + eps))
|
||||
return iou - (rho2 / c2 + v * alpha) # CIoU
|
||||
else: # GIoU https://arxiv.org/pdf/1902.09630.pdf
|
||||
c_area = cw * ch + eps # convex area
|
||||
@@ -322,11 +418,12 @@ def wh_iou(wh1, wh2):
|
||||
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
||||
|
||||
|
||||
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()):
|
||||
"""Performs Non-Maximum Suppression (NMS) on inference results
|
||||
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
|
||||
labels=()):
|
||||
"""Runs Non-Maximum Suppression (NMS) on inference results
|
||||
|
||||
Returns:
|
||||
detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
|
||||
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
|
||||
"""
|
||||
|
||||
nc = prediction.shape[2] - 5 # number of classes
|
||||
@@ -338,7 +435,7 @@ def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=Non
|
||||
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
|
||||
time_limit = 10.0 # seconds to quit after
|
||||
redundant = True # require redundant detections
|
||||
multi_label = nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
|
||||
merge = False # use merge-NMS
|
||||
|
||||
t = time.time()
|
||||
@@ -412,18 +509,20 @@ def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=Non
|
||||
return output
|
||||
|
||||
|
||||
def strip_optimizer(f='weights/best.pt', s=''): # from utils.general import *; strip_optimizer()
|
||||
def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_optimizer()
|
||||
# Strip optimizer from 'f' to finalize training, optionally save as 's'
|
||||
x = torch.load(f, map_location=torch.device('cpu'))
|
||||
for key in 'optimizer', 'training_results', 'wandb_id':
|
||||
x[key] = None
|
||||
if x.get('ema'):
|
||||
x['model'] = x['ema'] # replace model with ema
|
||||
for k in 'optimizer', 'training_results', 'wandb_id', 'ema', 'updates': # keys
|
||||
x[k] = None
|
||||
x['epoch'] = -1
|
||||
x['model'].half() # to FP16
|
||||
for p in x['model'].parameters():
|
||||
p.requires_grad = False
|
||||
torch.save(x, s or f)
|
||||
mb = os.path.getsize(s or f) / 1E6 # filesize
|
||||
print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
|
||||
print(f"Optimizer stripped from {f},{(' saved as %s,' % s) if s else ''} {mb:.1f}MB")
|
||||
|
||||
|
||||
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
||||
|
||||
@@ -21,13 +21,13 @@ def attempt_download(file, repo='ultralytics/yolov3'):
|
||||
file = Path(str(file).strip().replace("'", '').lower())
|
||||
|
||||
if not file.exists():
|
||||
# try:
|
||||
# response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest').json() # github api
|
||||
# assets = [x['name'] for x in response['assets']] # release assets, i.e. ['yolov5s.pt', 'yolov5m.pt', ...]
|
||||
# tag = response['tag_name'] # i.e. 'v1.0'
|
||||
# except: # fallback plan
|
||||
assets = ['yolov3.pt', 'yolov3-spp.pt', 'yolov3-tiny.pt']
|
||||
tag = subprocess.check_output('git tag', shell=True).decode().split()[-1]
|
||||
try:
|
||||
response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest').json() # github api
|
||||
assets = [x['name'] for x in response['assets']] # release assets, i.e. ['yolov5s.pt', 'yolov5m.pt', ...]
|
||||
tag = response['tag_name'] # i.e. 'v1.0'
|
||||
except: # fallback plan
|
||||
assets = ['yolov3.pt', 'yolov3-spp.pt', 'yolov3-tiny.pt']
|
||||
tag = subprocess.check_output('git tag', shell=True).decode().split()[-1]
|
||||
|
||||
name = file.name
|
||||
if name in assets:
|
||||
|
||||
+108
-93
@@ -85,117 +85,132 @@ class QFocalLoss(nn.Module):
|
||||
return loss
|
||||
|
||||
|
||||
def compute_loss(p, targets, model): # predictions, targets, model
|
||||
device = targets.device
|
||||
lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
|
||||
tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets
|
||||
h = model.hyp # hyperparameters
|
||||
class ComputeLoss:
|
||||
# Compute losses
|
||||
def __init__(self, model, autobalance=False):
|
||||
super(ComputeLoss, self).__init__()
|
||||
device = next(model.parameters()).device # get model device
|
||||
h = model.hyp # hyperparameters
|
||||
|
||||
# Define criteria
|
||||
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device)) # weight=model.class_weights)
|
||||
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
|
||||
# Define criteria
|
||||
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
|
||||
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
|
||||
|
||||
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
|
||||
cp, cn = smooth_BCE(eps=0.0)
|
||||
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
|
||||
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets
|
||||
|
||||
# Focal loss
|
||||
g = h['fl_gamma'] # focal loss gamma
|
||||
if g > 0:
|
||||
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
|
||||
# Focal loss
|
||||
g = h['fl_gamma'] # focal loss gamma
|
||||
if g > 0:
|
||||
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
|
||||
|
||||
# Losses
|
||||
balance = [4.0, 1.0, 0.4, 0.1] # P3-P6
|
||||
for i, pi in enumerate(p): # layer index, layer predictions
|
||||
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
|
||||
tobj = torch.zeros_like(pi[..., 0], device=device) # target obj
|
||||
det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module
|
||||
self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, .02]) # P3-P7
|
||||
self.ssi = list(det.stride).index(16) if autobalance else 0 # stride 16 index
|
||||
self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, model.gr, h, autobalance
|
||||
for k in 'na', 'nc', 'nl', 'anchors':
|
||||
setattr(self, k, getattr(det, k))
|
||||
|
||||
n = b.shape[0] # number of targets
|
||||
if n:
|
||||
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
|
||||
def __call__(self, p, targets): # predictions, targets, model
|
||||
device = targets.device
|
||||
lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
|
||||
tcls, tbox, indices, anchors = self.build_targets(p, targets) # targets
|
||||
|
||||
# Regression
|
||||
pxy = ps[:, :2].sigmoid() * 2. - 0.5
|
||||
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
|
||||
pbox = torch.cat((pxy, pwh), 1) # predicted box
|
||||
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target)
|
||||
lbox += (1.0 - iou).mean() # iou loss
|
||||
# Losses
|
||||
for i, pi in enumerate(p): # layer index, layer predictions
|
||||
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
|
||||
tobj = torch.zeros_like(pi[..., 0], device=device) # target obj
|
||||
|
||||
# Objectness
|
||||
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio
|
||||
n = b.shape[0] # number of targets
|
||||
if n:
|
||||
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
|
||||
|
||||
# Classification
|
||||
if model.nc > 1: # cls loss (only if multiple classes)
|
||||
t = torch.full_like(ps[:, 5:], cn, device=device) # targets
|
||||
t[range(n), tcls[i]] = cp
|
||||
lcls += BCEcls(ps[:, 5:], t) # BCE
|
||||
# Regression
|
||||
pxy = ps[:, :2].sigmoid() * 2. - 0.5
|
||||
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
|
||||
pbox = torch.cat((pxy, pwh), 1) # predicted box
|
||||
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target)
|
||||
lbox += (1.0 - iou).mean() # iou loss
|
||||
|
||||
# Append targets to text file
|
||||
# with open('targets.txt', 'a') as file:
|
||||
# [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
|
||||
# Objectness
|
||||
tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio
|
||||
|
||||
lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss
|
||||
# Classification
|
||||
if self.nc > 1: # cls loss (only if multiple classes)
|
||||
t = torch.full_like(ps[:, 5:], self.cn, device=device) # targets
|
||||
t[range(n), tcls[i]] = self.cp
|
||||
lcls += self.BCEcls(ps[:, 5:], t) # BCE
|
||||
|
||||
lbox *= h['box']
|
||||
lobj *= h['obj']
|
||||
lcls *= h['cls']
|
||||
bs = tobj.shape[0] # batch size
|
||||
# Append targets to text file
|
||||
# with open('targets.txt', 'a') as file:
|
||||
# [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
|
||||
|
||||
loss = lbox + lobj + lcls
|
||||
return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
|
||||
obji = self.BCEobj(pi[..., 4], tobj)
|
||||
lobj += obji * self.balance[i] # obj loss
|
||||
if self.autobalance:
|
||||
self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()
|
||||
|
||||
if self.autobalance:
|
||||
self.balance = [x / self.balance[self.ssi] for x in self.balance]
|
||||
lbox *= self.hyp['box']
|
||||
lobj *= self.hyp['obj']
|
||||
lcls *= self.hyp['cls']
|
||||
bs = tobj.shape[0] # batch size
|
||||
|
||||
def build_targets(p, targets, model):
|
||||
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
|
||||
det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module
|
||||
na, nt = det.na, targets.shape[0] # number of anchors, targets
|
||||
tcls, tbox, indices, anch = [], [], [], []
|
||||
gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
|
||||
ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
|
||||
targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices
|
||||
loss = lbox + lobj + lcls
|
||||
return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
|
||||
|
||||
g = 0.5 # bias
|
||||
off = torch.tensor([[0, 0],
|
||||
# [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
|
||||
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
|
||||
], device=targets.device).float() * g # offsets
|
||||
def build_targets(self, p, targets):
|
||||
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
|
||||
na, nt = self.na, targets.shape[0] # number of anchors, targets
|
||||
tcls, tbox, indices, anch = [], [], [], []
|
||||
gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
|
||||
ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
|
||||
targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices
|
||||
|
||||
for i in range(det.nl):
|
||||
anchors = det.anchors[i]
|
||||
gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
|
||||
g = 0.5 # bias
|
||||
off = torch.tensor([[0, 0],
|
||||
# [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
|
||||
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
|
||||
], device=targets.device).float() * g # offsets
|
||||
|
||||
# Match targets to anchors
|
||||
t = targets * gain
|
||||
if nt:
|
||||
# Matches
|
||||
r = t[:, :, 4:6] / anchors[:, None] # wh ratio
|
||||
j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare
|
||||
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
|
||||
t = t[j] # filter
|
||||
for i in range(self.nl):
|
||||
anchors = self.anchors[i]
|
||||
gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
|
||||
|
||||
# Offsets
|
||||
# Match targets to anchors
|
||||
t = targets * gain
|
||||
if nt:
|
||||
# Matches
|
||||
r = t[:, :, 4:6] / anchors[:, None] # wh ratio
|
||||
j = torch.max(r, 1. / r).max(2)[0] < self.hyp['anchor_t'] # compare
|
||||
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
|
||||
t = t[j] # filter
|
||||
|
||||
# Offsets
|
||||
gxy = t[:, 2:4] # grid xy
|
||||
gxi = gain[[2, 3]] - gxy # inverse
|
||||
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
|
||||
l, m = ((gxi % 1. < g) & (gxi > 1.)).T
|
||||
j = torch.stack((torch.ones_like(j),))
|
||||
t = t.repeat((off.shape[0], 1, 1))[j]
|
||||
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
|
||||
else:
|
||||
t = targets[0]
|
||||
offsets = 0
|
||||
|
||||
# Define
|
||||
b, c = t[:, :2].long().T # image, class
|
||||
gxy = t[:, 2:4] # grid xy
|
||||
gxi = gain[[2, 3]] - gxy # inverse
|
||||
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
|
||||
l, m = ((gxi % 1. < g) & (gxi > 1.)).T
|
||||
j = torch.stack((torch.ones_like(j),))
|
||||
t = t.repeat((off.shape[0], 1, 1))[j]
|
||||
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
|
||||
else:
|
||||
t = targets[0]
|
||||
offsets = 0
|
||||
gwh = t[:, 4:6] # grid wh
|
||||
gij = (gxy - offsets).long()
|
||||
gi, gj = gij.T # grid xy indices
|
||||
|
||||
# Define
|
||||
b, c = t[:, :2].long().T # image, class
|
||||
gxy = t[:, 2:4] # grid xy
|
||||
gwh = t[:, 4:6] # grid wh
|
||||
gij = (gxy - offsets).long()
|
||||
gi, gj = gij.T # grid xy indices
|
||||
# Append
|
||||
a = t[:, 6].long() # anchor indices
|
||||
indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices
|
||||
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
|
||||
anch.append(anchors[a]) # anchors
|
||||
tcls.append(c) # class
|
||||
|
||||
# Append
|
||||
a = t[:, 6].long() # anchor indices
|
||||
indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices
|
||||
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
|
||||
anch.append(anchors[a]) # anchors
|
||||
tcls.append(c) # class
|
||||
|
||||
return tcls, tbox, indices, anch
|
||||
return tcls, tbox, indices, anch
|
||||
|
||||
+42
-19
@@ -15,7 +15,7 @@ def fitness(x):
|
||||
return (x[:, :4] * w).sum(1)
|
||||
|
||||
|
||||
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision-recall_curve.png', names=[]):
|
||||
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=()):
|
||||
""" Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
# Arguments
|
||||
@@ -35,12 +35,11 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision
|
||||
|
||||
# Find unique classes
|
||||
unique_classes = np.unique(target_cls)
|
||||
nc = unique_classes.shape[0] # number of classes, number of detections
|
||||
|
||||
# Create Precision-Recall curve and compute AP for each class
|
||||
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
||||
pr_score = 0.1 # score to evaluate P and R https://github.com/ultralytics/yolov3/issues/898
|
||||
s = [unique_classes.shape[0], tp.shape[1]] # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95)
|
||||
ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s)
|
||||
ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
|
||||
for ci, c in enumerate(unique_classes):
|
||||
i = pred_cls == c
|
||||
n_l = (target_cls == c).sum() # number of labels
|
||||
@@ -55,25 +54,28 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision
|
||||
|
||||
# Recall
|
||||
recall = tpc / (n_l + 1e-16) # recall curve
|
||||
r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) # r at pr_score, negative x, xp because xp decreases
|
||||
r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases
|
||||
|
||||
# Precision
|
||||
precision = tpc / (tpc + fpc) # precision curve
|
||||
p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0]) # p at pr_score
|
||||
p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) # p at pr_score
|
||||
|
||||
# AP from recall-precision curve
|
||||
for j in range(tp.shape[1]):
|
||||
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])
|
||||
if plot and (j == 0):
|
||||
if plot and j == 0:
|
||||
py.append(np.interp(px, mrec, mpre)) # precision at mAP@0.5
|
||||
|
||||
# Compute F1 score (harmonic mean of precision and recall)
|
||||
# Compute F1 (harmonic mean of precision and recall)
|
||||
f1 = 2 * p * r / (p + r + 1e-16)
|
||||
|
||||
if plot:
|
||||
plot_pr_curve(px, py, ap, save_dir, names)
|
||||
plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
|
||||
plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
|
||||
plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
|
||||
plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')
|
||||
|
||||
return p, r, ap, f1, unique_classes.astype('int32')
|
||||
i = f1.mean(0).argmax() # max F1 index
|
||||
return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32')
|
||||
|
||||
|
||||
def compute_ap(recall, precision):
|
||||
@@ -145,12 +147,12 @@ class ConfusionMatrix:
|
||||
if n and sum(j) == 1:
|
||||
self.matrix[gc, detection_classes[m1[j]]] += 1 # correct
|
||||
else:
|
||||
self.matrix[gc, self.nc] += 1 # background FP
|
||||
self.matrix[self.nc, gc] += 1 # background FP
|
||||
|
||||
if n:
|
||||
for i, dc in enumerate(detection_classes):
|
||||
if not any(m1 == i):
|
||||
self.matrix[self.nc, dc] += 1 # background FN
|
||||
self.matrix[dc, self.nc] += 1 # background FN
|
||||
|
||||
def matrix(self):
|
||||
return self.matrix
|
||||
@@ -166,8 +168,8 @@ class ConfusionMatrix:
|
||||
sn.set(font_scale=1.0 if self.nc < 50 else 0.8) # for label size
|
||||
labels = (0 < len(names) < 99) and len(names) == self.nc # apply names to ticklabels
|
||||
sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True,
|
||||
xticklabels=names + ['background FN'] if labels else "auto",
|
||||
yticklabels=names + ['background FP'] if labels else "auto").set_facecolor((1, 1, 1))
|
||||
xticklabels=names + ['background FP'] if labels else "auto",
|
||||
yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1))
|
||||
fig.axes[0].set_xlabel('True')
|
||||
fig.axes[0].set_ylabel('Predicted')
|
||||
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
|
||||
@@ -181,13 +183,14 @@ class ConfusionMatrix:
|
||||
|
||||
# Plots ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def plot_pr_curve(px, py, ap, save_dir='.', names=()):
|
||||
def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()):
|
||||
# Precision-recall curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
py = np.stack(py, axis=1)
|
||||
|
||||
if 0 < len(names) < 21: # show mAP in legend if < 10 classes
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py.T):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]} %.3f' % ap[i, 0]) # plot(recall, precision)
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]} {ap[i, 0]:.3f}') # plot(recall, precision)
|
||||
else:
|
||||
ax.plot(px, py, linewidth=1, color='grey') # plot(recall, precision)
|
||||
|
||||
@@ -197,4 +200,24 @@ def plot_pr_curve(px, py, ap, save_dir='.', names=()):
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir) / 'precision_recall_curve.png', dpi=250)
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
||||
|
||||
def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence', ylabel='Metric'):
|
||||
# Metric-confidence curve
|
||||
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
||||
|
||||
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
||||
for i, y in enumerate(py):
|
||||
ax.plot(px, y, linewidth=1, label=f'{names[i]}') # plot(confidence, metric)
|
||||
else:
|
||||
ax.plot(px, py.T, linewidth=1, color='grey') # plot(confidence, metric)
|
||||
|
||||
y = py.mean(0)
|
||||
ax.plot(px, y, linewidth=3, color='blue', label=f'all classes {y.max():.2f} at {px[y.argmax()]:.3f}')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
||||
fig.savefig(Path(save_dir), dpi=250)
|
||||
|
||||
+39
-19
@@ -15,7 +15,7 @@ import pandas as pd
|
||||
import seaborn as sns
|
||||
import torch
|
||||
import yaml
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
from utils.general import xywh2xyxy, xyxy2xywh
|
||||
@@ -31,7 +31,7 @@ def color_list():
|
||||
def hex2rgb(h):
|
||||
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
return [hex2rgb(h) for h in plt.rcParams['axes.prop_cycle'].by_key()['color']]
|
||||
return [hex2rgb(h) for h in matplotlib.colors.TABLEAU_COLORS.values()] # or BASE_ (8), CSS4_ (148), XKCD_ (949)
|
||||
|
||||
|
||||
def hist2d(x, y, n=100):
|
||||
@@ -54,7 +54,7 @@ def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
||||
return filtfilt(b, a, data) # forward-backward filter
|
||||
|
||||
|
||||
def plot_one_box(x, img, color=None, label=None, line_thickness=None):
|
||||
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
|
||||
# Plots one bounding box on image img
|
||||
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
||||
color = color or [random.randint(0, 255) for _ in range(3)]
|
||||
@@ -68,6 +68,20 @@ def plot_one_box(x, img, color=None, label=None, line_thickness=None):
|
||||
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
||||
|
||||
|
||||
def plot_one_box_PIL(box, img, color=None, label=None, line_thickness=None):
|
||||
img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(img)
|
||||
line_thickness = line_thickness or max(int(min(img.size) / 200), 2)
|
||||
draw.rectangle(box, width=line_thickness, outline=tuple(color)) # plot
|
||||
if label:
|
||||
fontsize = max(round(max(img.size) / 40), 12)
|
||||
font = ImageFont.truetype("Arial.ttf", fontsize)
|
||||
txt_width, txt_height = font.getsize(label)
|
||||
draw.rectangle([box[0], box[1] - txt_height + 4, box[0] + txt_width, box[1]], fill=tuple(color))
|
||||
draw.text((box[0], box[1] - txt_height + 1), label, fill=(255, 255, 255), font=font)
|
||||
return np.asarray(img)
|
||||
|
||||
|
||||
def plot_wh_methods(): # from utils.plots import *; plot_wh_methods()
|
||||
# Compares the two methods for width-height anchor multiplication
|
||||
# https://github.com/ultralytics/yolov3/issues/168
|
||||
@@ -223,38 +237,39 @@ def plot_targets_txt(): # from utils.plots import *; plot_targets_txt()
|
||||
plt.savefig('targets.jpg', dpi=200)
|
||||
|
||||
|
||||
def plot_study_txt(path='study/', x=None): # from utils.plots import *; plot_study_txt()
|
||||
def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_txt()
|
||||
# Plot study.txt generated by test.py
|
||||
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
||||
ax = ax.ravel()
|
||||
# ax = ax.ravel()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
||||
for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolov5s', 'yolov5m', 'yolov5l', 'yolov5x']]:
|
||||
# for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolov3-tiny', 'yolov3', 'yolov3-spp', 'yolov5l']]:
|
||||
for f in sorted(Path(path).glob('study*.txt')):
|
||||
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
||||
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
||||
s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
||||
for i in range(7):
|
||||
ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
||||
ax[i].set_title(s[i])
|
||||
# for i in range(7):
|
||||
# ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
||||
# ax[i].set_title(s[i])
|
||||
|
||||
j = y[3].argmax() + 1
|
||||
ax2.plot(y[6, :j], y[3, :j] * 1E2, '.-', linewidth=2, markersize=8,
|
||||
ax2.plot(y[6, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8,
|
||||
label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
||||
|
||||
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
||||
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
||||
|
||||
ax2.grid()
|
||||
ax2.set_yticks(np.arange(30, 60, 5))
|
||||
ax2.set_xlim(0, 30)
|
||||
ax2.set_ylim(29, 51)
|
||||
ax2.grid(alpha=0.2)
|
||||
ax2.set_yticks(np.arange(20, 60, 5))
|
||||
ax2.set_xlim(0, 57)
|
||||
ax2.set_ylim(15, 55)
|
||||
ax2.set_xlabel('GPU Speed (ms/img)')
|
||||
ax2.set_ylabel('COCO AP val')
|
||||
ax2.legend(loc='lower right')
|
||||
plt.savefig('test_study.png', dpi=300)
|
||||
plt.savefig(str(Path(path).name) + '.png', dpi=300)
|
||||
|
||||
|
||||
def plot_labels(labels, save_dir=Path(''), loggers=None):
|
||||
def plot_labels(labels, names=(), save_dir=Path(''), loggers=None):
|
||||
# plot dataset labels
|
||||
print('Plotting labels... ')
|
||||
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
||||
@@ -271,7 +286,12 @@ def plot_labels(labels, save_dir=Path(''), loggers=None):
|
||||
matplotlib.use('svg') # faster
|
||||
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
|
||||
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
||||
ax[0].set_xlabel('classes')
|
||||
ax[0].set_ylabel('instances')
|
||||
if 0 < len(names) < 30:
|
||||
ax[0].set_xticks(range(len(names)))
|
||||
ax[0].set_xticklabels(names, rotation=90, fontsize=10)
|
||||
else:
|
||||
ax[0].set_xlabel('classes')
|
||||
sns.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
|
||||
sns.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
|
||||
|
||||
@@ -295,13 +315,13 @@ def plot_labels(labels, save_dir=Path(''), loggers=None):
|
||||
# loggers
|
||||
for k, v in loggers.items() or {}:
|
||||
if k == 'wandb' and v:
|
||||
v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]})
|
||||
v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]}, commit=False)
|
||||
|
||||
|
||||
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.plots import *; plot_evolution()
|
||||
# Plot hyperparameter evolution results in evolve.txt
|
||||
with open(yaml_file) as f:
|
||||
hyp = yaml.load(f, Loader=yaml.FullLoader)
|
||||
hyp = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
x = np.loadtxt('evolve.txt', ndmin=2)
|
||||
f = fitness(x)
|
||||
# weights = (f - f.min()) ** 2 # for weighted results
|
||||
|
||||
+20
-11
@@ -1,8 +1,10 @@
|
||||
# PyTorch utils
|
||||
# YOLOv3 PyTorch utils
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
@@ -43,17 +45,24 @@ def init_torch_seeds(seed=0):
|
||||
cudnn.benchmark, cudnn.deterministic = True, False
|
||||
|
||||
|
||||
def git_describe():
|
||||
def date_modified(path=__file__):
|
||||
# return human-readable file modification date, i.e. '2021-3-26'
|
||||
t = datetime.datetime.fromtimestamp(Path(path).stat().st_mtime)
|
||||
return f'{t.year}-{t.month}-{t.day}'
|
||||
|
||||
|
||||
def git_describe(path=Path(__file__).parent): # path must be a directory
|
||||
# return human-readable git description, i.e. v5.0-5-g3e25f1e https://git-scm.com/docs/git-describe
|
||||
if Path('.git').exists():
|
||||
return subprocess.check_output('git describe --tags --long --always', shell=True).decode('utf-8')[:-1]
|
||||
else:
|
||||
return ''
|
||||
s = f'git -C {path} describe --tags --long --always'
|
||||
try:
|
||||
return subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).decode()[:-1]
|
||||
except subprocess.CalledProcessError as e:
|
||||
return '' # not a git repository
|
||||
|
||||
|
||||
def select_device(device='', batch_size=None):
|
||||
# device = 'cpu' or '0' or '0,1,2,3'
|
||||
s = f'YOLOv3 {git_describe()} torch {torch.__version__} ' # string
|
||||
s = f'YOLOv3 🚀 {git_describe() or date_modified()} torch {torch.__version__} ' # string
|
||||
cpu = device.lower() == 'cpu'
|
||||
if cpu:
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False
|
||||
@@ -73,7 +82,7 @@ def select_device(device='', batch_size=None):
|
||||
else:
|
||||
s += 'CPU\n'
|
||||
|
||||
logger.info(s) # skip a line
|
||||
logger.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
||||
return torch.device('cuda:0' if cuda else 'cpu')
|
||||
|
||||
|
||||
@@ -120,7 +129,7 @@ def profile(x, ops, n=100, device=None):
|
||||
s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list'
|
||||
s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list'
|
||||
p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters
|
||||
print(f'{p:12.4g}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}')
|
||||
print(f'{p:12}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}')
|
||||
|
||||
|
||||
def is_parallel(model):
|
||||
@@ -182,7 +191,7 @@ def fuse_conv_and_bn(conv, bn):
|
||||
# prepare filters
|
||||
w_conv = conv.weight.clone().view(conv.out_channels, -1)
|
||||
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
|
||||
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.size()))
|
||||
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))
|
||||
|
||||
# prepare spatial bias
|
||||
b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias
|
||||
@@ -205,7 +214,7 @@ def model_info(model, verbose=False, img_size=640):
|
||||
|
||||
try: # FLOPS
|
||||
from thop import profile
|
||||
stride = int(model.stride.max()) if hasattr(model, 'stride') else 32
|
||||
stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32
|
||||
img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input
|
||||
flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPS
|
||||
img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import argparse
|
||||
|
||||
import yaml
|
||||
|
||||
from wandb_utils import WandbLogger
|
||||
|
||||
WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
|
||||
|
||||
|
||||
def create_dataset_artifact(opt):
|
||||
with open(opt.data) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # data dict
|
||||
logger = WandbLogger(opt, '', None, data, job_type='Dataset Creation')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--data', type=str, default='data/coco128.yaml', help='data.yaml path')
|
||||
parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset')
|
||||
parser.add_argument('--project', type=str, default='YOLOv5', help='name of W&B Project')
|
||||
opt = parser.parse_args()
|
||||
opt.resume = False # Explicitly disallow resume check for dataset upload job
|
||||
|
||||
create_dataset_artifact(opt)
|
||||
@@ -0,0 +1,306 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
from tqdm import tqdm
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent)) # add utils/ to path
|
||||
from utils.datasets import LoadImagesAndLabels
|
||||
from utils.datasets import img2label_paths
|
||||
from utils.general import colorstr, xywh2xyxy, check_dataset
|
||||
|
||||
try:
|
||||
import wandb
|
||||
from wandb import init, finish
|
||||
except ImportError:
|
||||
wandb = None
|
||||
|
||||
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 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
|
||||
model_artifact_name = 'run_' + run_id + '_model'
|
||||
return run_id, project, model_artifact_name
|
||||
|
||||
|
||||
def check_wandb_resume(opt):
|
||||
process_wandb_config_ddp_mode(opt) if opt.global_rank not in [-1, 0] else None
|
||||
if isinstance(opt.resume, str):
|
||||
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
if opt.global_rank not in [-1, 0]: # For resuming DDP runs
|
||||
run_id, project, model_artifact_name = get_run_info(opt.resume)
|
||||
api = wandb.Api()
|
||||
artifact = api.artifact(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(opt.data) as f:
|
||||
data_dict = yaml.load(f, Loader=yaml.SafeLoader) # 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.dump(data_dict, f)
|
||||
opt.data = ddp_data_path
|
||||
|
||||
|
||||
class WandbLogger():
|
||||
def __init__(self, opt, name, run_id, data_dict, job_type='Training'):
|
||||
# Pre-training routine --
|
||||
self.job_type = job_type
|
||||
self.wandb, self.wandb_run, self.data_dict = wandb, None if not wandb else wandb.run, data_dict
|
||||
# It's more elegant to stick to 1 wandb.init call, 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):
|
||||
run_id, project, 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, resume='allow')
|
||||
opt.resume = model_artifact_name
|
||||
elif self.wandb:
|
||||
self.wandb_run = wandb.init(config=opt,
|
||||
resume="allow",
|
||||
project='YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem,
|
||||
name=name,
|
||||
job_type=job_type,
|
||||
id=run_id) if not wandb.run else wandb.run
|
||||
if self.wandb_run:
|
||||
if self.job_type == 'Training':
|
||||
if not opt.resume:
|
||||
wandb_data_dict = self.check_and_upload_dataset(opt) if opt.upload_dataset else data_dict
|
||||
# Info useful for resuming from artifacts
|
||||
self.wandb_run.config.opt = vars(opt)
|
||||
self.wandb_run.config.data_dict = wandb_data_dict
|
||||
self.data_dict = self.setup_training(opt, data_dict)
|
||||
if self.job_type == 'Dataset Creation':
|
||||
self.data_dict = self.check_and_upload_dataset(opt)
|
||||
else:
|
||||
prefix = colorstr('wandb: ')
|
||||
print(f"{prefix}Install Weights & Biases for YOLOv5 logging with 'pip install wandb' (recommended)")
|
||||
|
||||
def check_and_upload_dataset(self, opt):
|
||||
assert wandb, 'Install wandb to upload dataset'
|
||||
check_dataset(self.data_dict)
|
||||
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)
|
||||
with open(config_path) as f:
|
||||
wandb_data_dict = yaml.load(f, Loader=yaml.SafeLoader)
|
||||
return wandb_data_dict
|
||||
|
||||
def setup_training(self, opt, data_dict):
|
||||
self.log_dict, self.current_epoch, self.log_imgs = {}, 0, 16 # Logging Constants
|
||||
self.bbox_interval = opt.bbox_interval
|
||||
if isinstance(opt.resume, str):
|
||||
modeldir, _ = self.download_model_artifact(opt)
|
||||
if modeldir:
|
||||
self.weights = Path(modeldir) / "last.pt"
|
||||
config = self.wandb_run.config
|
||||
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp = str(
|
||||
self.weights), config.save_period, config.total_batch_size, config.bbox_interval, config.epochs, \
|
||||
config.opt['hyp']
|
||||
data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume
|
||||
if 'val_artifact' not in self.__dict__: # 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)
|
||||
self.result_artifact, self.result_table, self.val_table, self.weights = None, None, None, None
|
||||
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)
|
||||
self.val_table = self.val_artifact.get("val")
|
||||
self.map_val_table_path()
|
||||
if self.val_artifact is not None:
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
if opt.bbox_interval == -1:
|
||||
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
|
||||
return data_dict
|
||||
|
||||
def download_dataset_artifact(self, path, alias):
|
||||
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
|
||||
dataset_artifact = wandb.use_artifact(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
|
||||
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):
|
||||
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')
|
||||
assert epochs_trained < total_epochs, 'training to %g epochs is finished, nothing to resume.' % (
|
||||
total_epochs)
|
||||
return modeldir, model_artifact
|
||||
return None, None
|
||||
|
||||
def log_model(self, path, opt, epoch, fitness_score, best_model=False):
|
||||
model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', type='model', metadata={
|
||||
'original_url': str(path),
|
||||
'epochs_trained': epoch + 1,
|
||||
'save period': opt.save_period,
|
||||
'project': opt.project,
|
||||
'total_epochs': opt.epochs,
|
||||
'fitness_score': fitness_score
|
||||
})
|
||||
model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
|
||||
wandb.log_artifact(model_artifact,
|
||||
aliases=['latest', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
|
||||
print("Saving model artifact on epoch ", epoch + 1)
|
||||
|
||||
def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False):
|
||||
with open(data_file) as f:
|
||||
data = yaml.load(f, Loader=yaml.SafeLoader) # 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
|
||||
self.train_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['train']), names, name='train') if data.get('train') else None
|
||||
self.val_artifact = self.create_dataset_table(LoadImagesAndLabels(
|
||||
data['val']), names, name='val') if data.get('val') else None
|
||||
if data.get('train'):
|
||||
data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train')
|
||||
if data.get('val'):
|
||||
data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val')
|
||||
path = data_file if overwrite_config else '_wandb.'.join(data_file.rsplit('.', 1)) # updated data.yaml path
|
||||
data.pop('download', None)
|
||||
with open(path, 'w') as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
if self.job_type == 'Training': # builds correct artifact pipeline graph
|
||||
self.wandb_run.use_artifact(self.val_artifact)
|
||||
self.wandb_run.use_artifact(self.train_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):
|
||||
self.val_table_map = {}
|
||||
print("Mapping dataset")
|
||||
for i, data in enumerate(tqdm(self.val_table.data)):
|
||||
self.val_table_map[data[3]] = data[0]
|
||||
|
||||
def create_dataset_table(self, dataset, class_to_id, name='dataset'):
|
||||
# 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.img_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)):
|
||||
height, width = shapes[0]
|
||||
labels[:, 2:] = (xywh2xyxy(labels[:, 2:].view(-1, 4))) * torch.Tensor([width, height, width, height])
|
||||
box_data, img_classes = [], {}
|
||||
for cls, *xyxy in labels[:, 1:].tolist():
|
||||
cls = int(cls)
|
||||
box_data.append({"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": cls,
|
||||
"box_caption": "%s" % (class_to_id[cls]),
|
||||
"scores": {"acc": 1},
|
||||
"domain": "pixel"})
|
||||
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), json.dumps(img_classes),
|
||||
Path(paths).name)
|
||||
artifact.add(table, name)
|
||||
return artifact
|
||||
|
||||
def log_training_progress(self, predn, path, names):
|
||||
if self.val_table and self.result_table:
|
||||
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
|
||||
box_data = []
|
||||
total_conf = 0
|
||||
for *xyxy, conf, cls in predn.tolist():
|
||||
if conf >= 0.25:
|
||||
box_data.append(
|
||||
{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
||||
"class_id": int(cls),
|
||||
"box_caption": "%s %.3f" % (names[cls], conf),
|
||||
"scores": {"class_score": conf},
|
||||
"domain": "pixel"})
|
||||
total_conf = total_conf + conf
|
||||
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
||||
id = self.val_table_map[Path(path).name]
|
||||
self.result_table.add_data(self.current_epoch,
|
||||
id,
|
||||
wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set),
|
||||
total_conf / max(1, len(box_data))
|
||||
)
|
||||
|
||||
def log(self, log_dict):
|
||||
if self.wandb_run:
|
||||
for key, value in log_dict.items():
|
||||
self.log_dict[key] = value
|
||||
|
||||
def end_epoch(self, best_result=False):
|
||||
if self.wandb_run:
|
||||
wandb.log(self.log_dict)
|
||||
self.log_dict = {}
|
||||
if self.result_artifact:
|
||||
train_results = wandb.JoinedTable(self.val_table, self.result_table, "id")
|
||||
self.result_artifact.add(train_results, 'result')
|
||||
wandb.log_artifact(self.result_artifact, aliases=['latest', 'epoch ' + str(self.current_epoch),
|
||||
('best' if best_result else '')])
|
||||
self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"])
|
||||
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
||||
|
||||
def finish_run(self):
|
||||
if self.wandb_run:
|
||||
if self.log_dict:
|
||||
wandb.log(self.log_dict)
|
||||
wandb.run.finish()
|
||||
Reference in New Issue
Block a user