`ultralytics 8.0.80` single-line docstring fixes (#2060)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
single_channel
Glenn Jocher 2 years ago committed by GitHub
parent 31db8ed163
commit 5bce1c3021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
# Ultralytics YOLO 🚀, AGPL-3.0 license # Ultralytics YOLO 🚀, AGPL-3.0 license
__version__ = '8.0.79' __version__ = '8.0.80'
from ultralytics.hub import start from ultralytics.hub import start
from ultralytics.yolo.engine.model import YOLO from ultralytics.yolo.engine.model import YOLO

@ -54,7 +54,7 @@ model.train()""")
def reset_model(model_id=''): def reset_model(model_id=''):
# Reset a trained model to an untrained state """Reset a trained model to an untrained state."""
r = requests.post('https://api.ultralytics.com/model-reset', json={'apiKey': Auth().api_key, 'modelId': model_id}) r = requests.post('https://api.ultralytics.com/model-reset', json={'apiKey': Auth().api_key, 'modelId': model_id})
if r.status_code == 200: if r.status_code == 200:
LOGGER.info(f'{PREFIX}Model reset successfully') LOGGER.info(f'{PREFIX}Model reset successfully')
@ -63,13 +63,13 @@ def reset_model(model_id=''):
def export_fmts_hub(): def export_fmts_hub():
# Returns a list of HUB-supported export formats """Returns a list of HUB-supported export formats."""
from ultralytics.yolo.engine.exporter import export_formats from ultralytics.yolo.engine.exporter import export_formats
return list(export_formats()['Argument'][1:]) + ['ultralytics_tflite', 'ultralytics_coreml'] return list(export_formats()['Argument'][1:]) + ['ultralytics_tflite', 'ultralytics_coreml']
def export_model(model_id='', format='torchscript'): def export_model(model_id='', format='torchscript'):
# Export a model to all formats """Export a model to all formats."""
assert format in export_fmts_hub(), f"Unsupported export format '{format}', valid formats are {export_fmts_hub()}" assert format in export_fmts_hub(), f"Unsupported export format '{format}', valid formats are {export_fmts_hub()}"
r = requests.post('https://api.ultralytics.com/export', r = requests.post('https://api.ultralytics.com/export',
json={ json={
@ -81,7 +81,7 @@ def export_model(model_id='', format='torchscript'):
def get_export(model_id='', format='torchscript'): def get_export(model_id='', format='torchscript'):
# Get an exported model dictionary with download URL """Get an exported model dictionary with download URL."""
assert format in export_fmts_hub(), f"Unsupported export format '{format}', valid formats are {export_fmts_hub()}" assert format in export_fmts_hub(), f"Unsupported export format '{format}', valid formats are {export_fmts_hub()}"
r = requests.post('https://api.ultralytics.com/get-export', r = requests.post('https://api.ultralytics.com/get-export',
json={ json={

@ -124,7 +124,7 @@ class HUBTrainingSession:
'device': data['device'], 'device': data['device'],
'cache': data['cache'], 'cache': data['cache'],
'data': data['data']} 'data': data['data']}
self.model_file = data.get('cfg', data['weights']) self.model_file = data.get('cfg') or data.get('weights') # cfg for pretrained=False
self.model_file = checks.check_yolov5u_filename(self.model_file, verbose=False) # YOLOv5->YOLOv5u self.model_file = checks.check_yolov5u_filename(self.model_file, verbose=False) # YOLOv5->YOLOv5u
elif data['status'] == 'training': # existing model to resume training elif data['status'] == 'training': # existing model to resume training
self.train_args = {'data': data['data'], 'resume': True} self.train_args = {'data': data['data'], 'resume': True}

@ -21,11 +21,11 @@ from ultralytics.yolo.utils.ops import xywh2xyxy
def check_class_names(names): def check_class_names(names):
# Check class names. Map imagenet class codes to human-readable names if required. Convert lists to dicts. """Check class names. Map imagenet class codes to human-readable names if required. Convert lists to dicts."""
if isinstance(names, list): # names is a list if isinstance(names, list): # names is a list
names = dict(enumerate(names)) # convert to dict names = dict(enumerate(names)) # convert to dict
if isinstance(names, dict): if isinstance(names, dict):
# convert 1) string keys to int, i.e. '0' to 0, and non-string values to strings, i.e. True to 'True' # Convert 1) string keys to int, i.e. '0' to 0, and non-string values to strings, i.e. True to 'True'
names = {int(k): str(v) for k, v in names.items()} names = {int(k): str(v) for k, v in names.items()}
n = len(names) n = len(names)
if max(names.keys()) >= n: if max(names.keys()) >= n:
@ -229,7 +229,7 @@ class AutoBackend(nn.Module):
interpreter.allocate_tensors() # allocate interpreter.allocate_tensors() # allocate
input_details = interpreter.get_input_details() # inputs input_details = interpreter.get_input_details() # inputs
output_details = interpreter.get_output_details() # outputs output_details = interpreter.get_output_details() # outputs
# load metadata # Load metadata
with contextlib.suppress(zipfile.BadZipFile): with contextlib.suppress(zipfile.BadZipFile):
with zipfile.ZipFile(w, 'r') as model: with zipfile.ZipFile(w, 'r') as model:
meta_file = model.namelist()[0] meta_file = model.namelist()[0]

@ -24,7 +24,7 @@ from ultralytics.yolo.utils.torch_utils import copy_attr, smart_inference_mode
class AutoShape(nn.Module): class AutoShape(nn.Module):
# YOLOv8 input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS """YOLOv8 input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS."""
conf = 0.25 # NMS confidence threshold conf = 0.25 # NMS confidence threshold
iou = 0.45 # NMS IoU threshold iou = 0.45 # NMS IoU threshold
agnostic = False # NMS class-agnostic agnostic = False # NMS class-agnostic
@ -47,7 +47,7 @@ class AutoShape(nn.Module):
m.export = True # do not output loss values m.export = True # do not output loss values
def _apply(self, fn): def _apply(self, fn):
# Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers """Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers."""
self = super()._apply(fn) self = super()._apply(fn)
if self.pt: if self.pt:
m = self.model.model.model[-1] if self.dmb else self.model.model[-1] # Detect() m = self.model.model.model[-1] if self.dmb else self.model.model[-1] # Detect()
@ -59,7 +59,7 @@ class AutoShape(nn.Module):
@smart_inference_mode() @smart_inference_mode()
def forward(self, ims, size=640, augment=False, profile=False): def forward(self, ims, size=640, augment=False, profile=False):
# Inference from various sources. For size(height=640, width=1280), RGB images example inputs are: """Inference from various sources. For size(height=640, width=1280), RGB images example inputs are:."""
# file: ims = 'data/images/zidane.jpg' # str or PosixPath # file: ims = 'data/images/zidane.jpg' # str or PosixPath
# URI: = 'https://ultralytics.com/images/zidane.jpg' # URI: = 'https://ultralytics.com/images/zidane.jpg'
# OpenCV: = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(640,1280,3) # OpenCV: = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(640,1280,3)
@ -202,7 +202,7 @@ class Detections:
return self.ims return self.ims
def pandas(self): def pandas(self):
# return detections as pandas DataFrames, i.e. print(results.pandas().xyxy[0]) """Return detections as pandas DataFrames, i.e. print(results.pandas().xyxy[0])."""
import pandas import pandas
new = copy(self) # return copy new = copy(self) # return copy
ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name' # xyxy columns ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name' # xyxy columns
@ -213,7 +213,7 @@ class Detections:
return new return new
def tolist(self): def tolist(self):
# return a list of Detections objects, i.e. 'for result in results.tolist():' """Return a list of Detections objects, i.e. 'for result in results.tolist():'."""
r = range(self.n) # iterable r = range(self.n) # iterable
x = [Detections([self.ims[i]], [self.pred[i]], [self.files[i]], self.times, self.names, self.s) for i in r] x = [Detections([self.ims[i]], [self.pred[i]], [self.files[i]], self.times, self.names, self.s) for i in r]
# for d in x: # for d in x:

@ -12,7 +12,7 @@ from ultralytics.yolo.utils.tal import dist2bbox, make_anchors
def autopad(k, p=None, d=1): # kernel, padding, dilation def autopad(k, p=None, d=1): # kernel, padding, dilation
# Pad to 'same' shape outputs """Pad to 'same' shape outputs."""
if d > 1: if d > 1:
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
if p is None: if p is None:
@ -21,7 +21,7 @@ def autopad(k, p=None, d=1): # kernel, padding, dilation
class Conv(nn.Module): class Conv(nn.Module):
# Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation) """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
default_act = nn.SiLU() # default activation default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
@ -38,19 +38,21 @@ class Conv(nn.Module):
class DWConv(Conv): class DWConv(Conv):
# Depth-wise convolution """Depth-wise convolution."""
def __init__(self, c1, c2, k=1, s=1, d=1, act=True): # ch_in, ch_out, kernel, stride, dilation, activation def __init__(self, c1, c2, k=1, s=1, d=1, act=True): # ch_in, ch_out, kernel, stride, dilation, activation
super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act) super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)
class DWConvTranspose2d(nn.ConvTranspose2d): class DWConvTranspose2d(nn.ConvTranspose2d):
# Depth-wise transpose convolution """Depth-wise transpose convolution."""
def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0): # ch_in, ch_out, kernel, stride, padding, padding_out def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0): # ch_in, ch_out, kernel, stride, padding, padding_out
super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2)) super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))
class ConvTranspose(nn.Module): class ConvTranspose(nn.Module):
# Convolution transpose 2d layer """Convolution transpose 2d layer."""
default_act = nn.SiLU() # default activation default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=2, s=2, p=0, bn=True, act=True): def __init__(self, c1, c2, k=2, s=2, p=0, bn=True, act=True):
@ -67,8 +69,11 @@ class ConvTranspose(nn.Module):
class DFL(nn.Module): class DFL(nn.Module):
# Integral module of Distribution Focal Loss (DFL) """
# Proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391 Integral module of Distribution Focal Loss (DFL).
Proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391
"""
def __init__(self, c1=16): def __init__(self, c1=16):
super().__init__() super().__init__()
self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False) self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False)
@ -83,7 +88,8 @@ class DFL(nn.Module):
class TransformerLayer(nn.Module): class TransformerLayer(nn.Module):
# Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance) """Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance)."""
def __init__(self, c, num_heads): def __init__(self, c, num_heads):
super().__init__() super().__init__()
self.q = nn.Linear(c, c, bias=False) self.q = nn.Linear(c, c, bias=False)
@ -100,7 +106,8 @@ class TransformerLayer(nn.Module):
class TransformerBlock(nn.Module): class TransformerBlock(nn.Module):
# Vision Transformer https://arxiv.org/abs/2010.11929 """Vision Transformer https://arxiv.org/abs/2010.11929."""
def __init__(self, c1, c2, num_heads, num_layers): def __init__(self, c1, c2, num_heads, num_layers):
super().__init__() super().__init__()
self.conv = None self.conv = None
@ -119,7 +126,8 @@ class TransformerBlock(nn.Module):
class Bottleneck(nn.Module): class Bottleneck(nn.Module):
# Standard bottleneck """Standard bottleneck."""
def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5): # ch_in, ch_out, shortcut, groups, kernels, expand def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5): # ch_in, ch_out, shortcut, groups, kernels, expand
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
@ -132,7 +140,8 @@ class Bottleneck(nn.Module):
class BottleneckCSP(nn.Module): class BottleneckCSP(nn.Module):
# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks """CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
@ -151,7 +160,8 @@ class BottleneckCSP(nn.Module):
class C3(nn.Module): class C3(nn.Module):
# CSP Bottleneck with 3 convolutions """CSP Bottleneck with 3 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__() super().__init__()
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
@ -165,7 +175,8 @@ class C3(nn.Module):
class C2(nn.Module): class C2(nn.Module):
# CSP Bottleneck with 2 convolutions """CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__() super().__init__()
self.c = int(c2 * e) # hidden channels self.c = int(c2 * e) # hidden channels
@ -180,7 +191,8 @@ class C2(nn.Module):
class C2f(nn.Module): class C2f(nn.Module):
# CSP Bottleneck with 2 convolutions """CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__() super().__init__()
self.c = int(c2 * e) # hidden channels self.c = int(c2 * e) # hidden channels
@ -200,7 +212,8 @@ class C2f(nn.Module):
class ChannelAttention(nn.Module): class ChannelAttention(nn.Module):
# Channel-attention module https://github.com/open-mmlab/mmdetection/tree/v3.0.0rc1/configs/rtmdet """Channel-attention module https://github.com/open-mmlab/mmdetection/tree/v3.0.0rc1/configs/rtmdet."""
def __init__(self, channels: int) -> None: def __init__(self, channels: int) -> None:
super().__init__() super().__init__()
self.pool = nn.AdaptiveAvgPool2d(1) self.pool = nn.AdaptiveAvgPool2d(1)
@ -212,7 +225,8 @@ class ChannelAttention(nn.Module):
class SpatialAttention(nn.Module): class SpatialAttention(nn.Module):
# Spatial-attention module """Spatial-attention module."""
def __init__(self, kernel_size=7): def __init__(self, kernel_size=7):
super().__init__() super().__init__()
assert kernel_size in (3, 7), 'kernel size must be 3 or 7' assert kernel_size in (3, 7), 'kernel size must be 3 or 7'
@ -225,7 +239,8 @@ class SpatialAttention(nn.Module):
class CBAM(nn.Module): class CBAM(nn.Module):
# Convolutional Block Attention Module """Convolutional Block Attention Module."""
def __init__(self, c1, kernel_size=7): # ch_in, kernels def __init__(self, c1, kernel_size=7): # ch_in, kernels
super().__init__() super().__init__()
self.channel_attention = ChannelAttention(c1) self.channel_attention = ChannelAttention(c1)
@ -236,7 +251,8 @@ class CBAM(nn.Module):
class C1(nn.Module): class C1(nn.Module):
# CSP Bottleneck with 1 convolution """CSP Bottleneck with 1 convolution."""
def __init__(self, c1, c2, n=1): # ch_in, ch_out, number def __init__(self, c1, c2, n=1): # ch_in, ch_out, number
super().__init__() super().__init__()
self.cv1 = Conv(c1, c2, 1, 1) self.cv1 = Conv(c1, c2, 1, 1)
@ -248,7 +264,8 @@ class C1(nn.Module):
class C3x(C3): class C3x(C3):
# C3 module with cross-convolutions """C3 module with cross-convolutions."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e) super().__init__(c1, c2, n, shortcut, g, e)
self.c_ = int(c2 * e) self.c_ = int(c2 * e)
@ -256,7 +273,8 @@ class C3x(C3):
class C3TR(C3): class C3TR(C3):
# C3 module with TransformerBlock() """C3 module with TransformerBlock()."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e) super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e) c_ = int(c2 * e)
@ -264,7 +282,8 @@ class C3TR(C3):
class C3Ghost(C3): class C3Ghost(C3):
# C3 module with GhostBottleneck() """C3 module with GhostBottleneck()."""
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e) super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e) # hidden channels c_ = int(c2 * e) # hidden channels
@ -272,7 +291,8 @@ class C3Ghost(C3):
class SPP(nn.Module): class SPP(nn.Module):
# Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729 """Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729."""
def __init__(self, c1, c2, k=(5, 9, 13)): def __init__(self, c1, c2, k=(5, 9, 13)):
super().__init__() super().__init__()
c_ = c1 // 2 # hidden channels c_ = c1 // 2 # hidden channels
@ -286,7 +306,8 @@ class SPP(nn.Module):
class SPPF(nn.Module): class SPPF(nn.Module):
# Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher """Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher."""
def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13))
super().__init__() super().__init__()
c_ = c1 // 2 # hidden channels c_ = c1 // 2 # hidden channels
@ -302,7 +323,8 @@ class SPPF(nn.Module):
class Focus(nn.Module): class Focus(nn.Module):
# Focus wh information into c-space """Focus wh information into c-space."""
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__() super().__init__()
self.conv = Conv(c1 * 4, c2, k, s, p, g, act=act) self.conv = Conv(c1 * 4, c2, k, s, p, g, act=act)
@ -314,7 +336,8 @@ class Focus(nn.Module):
class GhostConv(nn.Module): class GhostConv(nn.Module):
# Ghost Convolution https://github.com/huawei-noah/ghostnet """Ghost Convolution https://github.com/huawei-noah/ghostnet."""
def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups
super().__init__() super().__init__()
c_ = c2 // 2 # hidden channels c_ = c2 // 2 # hidden channels
@ -327,7 +350,8 @@ class GhostConv(nn.Module):
class GhostBottleneck(nn.Module): class GhostBottleneck(nn.Module):
# Ghost Bottleneck https://github.com/huawei-noah/ghostnet """Ghost Bottleneck https://github.com/huawei-noah/ghostnet."""
def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride
super().__init__() super().__init__()
c_ = c2 // 2 c_ = c2 // 2
@ -343,7 +367,8 @@ class GhostBottleneck(nn.Module):
class Concat(nn.Module): class Concat(nn.Module):
# Concatenate a list of tensors along dimension """Concatenate a list of tensors along dimension."""
def __init__(self, dimension=1): def __init__(self, dimension=1):
super().__init__() super().__init__()
self.d = dimension self.d = dimension
@ -353,7 +378,8 @@ class Concat(nn.Module):
class Proto(nn.Module): class Proto(nn.Module):
# YOLOv8 mask Proto module for segmentation models """YOLOv8 mask Proto module for segmentation models."""
def __init__(self, c1, c_=256, c2=32): # ch_in, number of protos, number of masks def __init__(self, c1, c_=256, c2=32): # ch_in, number of protos, number of masks
super().__init__() super().__init__()
self.cv1 = Conv(c1, c_, k=3) self.cv1 = Conv(c1, c_, k=3)
@ -366,7 +392,8 @@ class Proto(nn.Module):
class Ensemble(nn.ModuleList): class Ensemble(nn.ModuleList):
# Ensemble of models """Ensemble of models."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -382,7 +409,7 @@ class Ensemble(nn.ModuleList):
class Detect(nn.Module): class Detect(nn.Module):
# YOLOv8 Detect head for detection models """YOLOv8 Detect head for detection models."""
dynamic = False # force grid reconstruction dynamic = False # force grid reconstruction
export = False # export mode export = False # export mode
shape = None shape = None
@ -423,7 +450,7 @@ class Detect(nn.Module):
return y if self.export else (y, x) return y if self.export else (y, x)
def bias_init(self): def bias_init(self):
# Initialize Detect() biases, WARNING: requires stride availability """Initialize Detect() biases, WARNING: requires stride availability."""
m = self # self.model[-1] # Detect() module m = self # self.model[-1] # Detect() module
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
# ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum()) # nominal class frequency # ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum()) # nominal class frequency
@ -433,7 +460,8 @@ class Detect(nn.Module):
class Segment(Detect): class Segment(Detect):
# YOLOv8 Segment head for segmentation models """YOLOv8 Segment head for segmentation models."""
def __init__(self, nc=80, nm=32, npr=256, ch=()): def __init__(self, nc=80, nm=32, npr=256, ch=()):
super().__init__(nc, ch) super().__init__(nc, ch)
self.nm = nm # number of masks self.nm = nm # number of masks
@ -456,7 +484,8 @@ class Segment(Detect):
class Pose(Detect): class Pose(Detect):
# YOLOv8 Pose head for keypoints models """YOLOv8 Pose head for keypoints models."""
def __init__(self, nc=80, kpt_shape=(17, 3), ch=()): def __init__(self, nc=80, kpt_shape=(17, 3), ch=()):
super().__init__(nc, ch) super().__init__(nc, ch)
self.kpt_shape = kpt_shape # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible) self.kpt_shape = kpt_shape # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
@ -486,7 +515,8 @@ class Pose(Detect):
class Classify(nn.Module): class Classify(nn.Module):
# YOLOv8 classification head, i.e. x(b,c1,20,20) to x(b,c2) """YOLOv8 classification head, i.e. x(b,c1,20,20) to x(b,c2)."""
def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in, ch_out, kernel, stride, padding, groups def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__() super().__init__()
c_ = 1280 # efficientnet_b0 size c_ = 1280 # efficientnet_b0 size

@ -167,7 +167,8 @@ class BaseModel(nn.Module):
class DetectionModel(BaseModel): class DetectionModel(BaseModel):
# YOLOv8 detection model """YOLOv8 detection model."""
def __init__(self, cfg='yolov8n.yaml', ch=3, nc=None, verbose=True): # model, input channels, number of classes def __init__(self, cfg='yolov8n.yaml', ch=3, nc=None, verbose=True): # model, input channels, number of classes
super().__init__() super().__init__()
self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg) # cfg dict self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg) # cfg dict
@ -218,7 +219,7 @@ class DetectionModel(BaseModel):
@staticmethod @staticmethod
def _descale_pred(p, flips, scale, img_size, dim=1): def _descale_pred(p, flips, scale, img_size, dim=1):
# de-scale predictions following augmented inference (inverse operation) """De-scale predictions following augmented inference (inverse operation)."""
p[:, :4] /= scale # de-scale p[:, :4] /= scale # de-scale
x, y, wh, cls = p.split((1, 1, 2, p.shape[dim] - 4), dim) x, y, wh, cls = p.split((1, 1, 2, p.shape[dim] - 4), dim)
if flips == 2: if flips == 2:
@ -228,7 +229,7 @@ class DetectionModel(BaseModel):
return torch.cat((x, y, wh, cls), dim) return torch.cat((x, y, wh, cls), dim)
def _clip_augmented(self, y): def _clip_augmented(self, y):
# Clip YOLOv5 augmented inference tails """Clip YOLOv5 augmented inference tails."""
nl = self.model[-1].nl # number of detection layers (P3-P5) nl = self.model[-1].nl # number of detection layers (P3-P5)
g = sum(4 ** x for x in range(nl)) # grid points g = sum(4 ** x for x in range(nl)) # grid points
e = 1 # exclude layer count e = 1 # exclude layer count
@ -240,7 +241,8 @@ class DetectionModel(BaseModel):
class SegmentationModel(DetectionModel): class SegmentationModel(DetectionModel):
# YOLOv8 segmentation model """YOLOv8 segmentation model."""
def __init__(self, cfg='yolov8n-seg.yaml', ch=3, nc=None, verbose=True): def __init__(self, cfg='yolov8n-seg.yaml', ch=3, nc=None, verbose=True):
super().__init__(cfg=cfg, ch=ch, nc=nc, verbose=verbose) super().__init__(cfg=cfg, ch=ch, nc=nc, verbose=verbose)
@ -249,7 +251,8 @@ class SegmentationModel(DetectionModel):
class PoseModel(DetectionModel): class PoseModel(DetectionModel):
# YOLOv8 pose model """YOLOv8 pose model."""
def __init__(self, cfg='yolov8n-pose.yaml', ch=3, nc=None, data_kpt_shape=(None, None), verbose=True): def __init__(self, cfg='yolov8n-pose.yaml', ch=3, nc=None, data_kpt_shape=(None, None), verbose=True):
if not isinstance(cfg, dict): if not isinstance(cfg, dict):
cfg = yaml_model_load(cfg) # load model YAML cfg = yaml_model_load(cfg) # load model YAML
@ -260,7 +263,8 @@ class PoseModel(DetectionModel):
class ClassificationModel(BaseModel): class ClassificationModel(BaseModel):
# YOLOv8 classification model """YOLOv8 classification model."""
def __init__(self, def __init__(self,
cfg=None, cfg=None,
model=None, model=None,
@ -272,7 +276,7 @@ class ClassificationModel(BaseModel):
self._from_detection_model(model, nc, cutoff) if model is not None else self._from_yaml(cfg, ch, nc, verbose) self._from_detection_model(model, nc, cutoff) if model is not None else self._from_yaml(cfg, ch, nc, verbose)
def _from_detection_model(self, model, nc=1000, cutoff=10): def _from_detection_model(self, model, nc=1000, cutoff=10):
# Create a YOLOv5 classification model from a YOLOv5 detection model """Create a YOLOv5 classification model from a YOLOv5 detection model."""
from ultralytics.nn.autobackend import AutoBackend from ultralytics.nn.autobackend import AutoBackend
if isinstance(model, AutoBackend): if isinstance(model, AutoBackend):
model = model.model # unwrap DetectMultiBackend model = model.model # unwrap DetectMultiBackend
@ -304,7 +308,7 @@ class ClassificationModel(BaseModel):
@staticmethod @staticmethod
def reshape_outputs(model, nc): def reshape_outputs(model, nc):
# Update a TorchVision classification model to class count 'n' if required """Update a TorchVision classification model to class count 'n' if required."""
name, m = list((model.model if hasattr(model, 'model') else model).named_children())[-1] # last module name, m = list((model.model if hasattr(model, 'model') else model).named_children())[-1] # last module
if isinstance(m, Classify): # YOLO Classify() head if isinstance(m, Classify): # YOLO Classify() head
if m.linear.out_features != nc: if m.linear.out_features != nc:
@ -363,7 +367,7 @@ def torch_safe_load(weight):
def attempt_load_weights(weights, device=None, inplace=True, fuse=False): def attempt_load_weights(weights, device=None, inplace=True, fuse=False):
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a """Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a."""
ensemble = Ensemble() ensemble = Ensemble()
for w in weights if isinstance(weights, list) else [weights]: for w in weights if isinstance(weights, list) else [weights]:
@ -403,7 +407,7 @@ def attempt_load_weights(weights, device=None, inplace=True, fuse=False):
def attempt_load_one_weight(weight, device=None, inplace=True, fuse=False): def attempt_load_one_weight(weight, device=None, inplace=True, fuse=False):
# Loads a single model weights """Loads a single model weights."""
ckpt, weight = torch_safe_load(weight) # load ckpt ckpt, weight = torch_safe_load(weight) # load ckpt
args = {**DEFAULT_CFG_DICT, **ckpt['train_args']} # combine model and default args, preferring model args args = {**DEFAULT_CFG_DICT, **ckpt['train_args']} # combine model and default args, preferring model args
model = (ckpt.get('ema') or ckpt['model']).to(device).float() # FP32 model model = (ckpt.get('ema') or ckpt['model']).to(device).float() # FP32 model
@ -546,7 +550,7 @@ def guess_model_task(model):
""" """
def cfg2task(cfg): def cfg2task(cfg):
# Guess from YAML dictionary """Guess from YAML dictionary."""
m = cfg['head'][-1][-2].lower() # output module name m = cfg['head'][-1][-2].lower() # output module name
if m in ('classify', 'classifier', 'cls', 'fc'): if m in ('classify', 'classifier', 'cls', 'fc'):
return 'classify' return 'classify'

@ -27,7 +27,7 @@ class BaseTrack:
frame_id = 0 frame_id = 0
time_since_update = 0 time_since_update = 0
# multi-camera # Multi-camera
location = (np.inf, np.inf) location = (np.inf, np.inf)
@property @property

@ -100,7 +100,7 @@ class BOTSORT(BYTETracker):
self.appearance_thresh = args.appearance_thresh self.appearance_thresh = args.appearance_thresh
if args.with_reid: if args.with_reid:
# haven't supported BoT-SORT(reid) yet # Haven't supported BoT-SORT(reid) yet
self.encoder = None self.encoder = None
# self.gmc = GMC(method=args.cmc_method, verbose=[args.name, args.ablation]) # self.gmc = GMC(method=args.cmc_method, verbose=[args.name, args.ablation])
self.gmc = GMC(method=args.cmc_method) self.gmc = GMC(method=args.cmc_method)

@ -11,8 +11,7 @@ class STrack(BaseTrack):
shared_kalman = KalmanFilterXYAH() shared_kalman = KalmanFilterXYAH()
def __init__(self, tlwh, score, cls): def __init__(self, tlwh, score, cls):
"""wait activate."""
# wait activate
self._tlwh = np.asarray(self.tlbr_to_tlwh(tlwh[:-1]), dtype=np.float32) self._tlwh = np.asarray(self.tlbr_to_tlwh(tlwh[:-1]), dtype=np.float32)
self.kalman_filter = None self.kalman_filter = None
self.mean, self.covariance = None, None self.mean, self.covariance = None, None
@ -62,7 +61,7 @@ class STrack(BaseTrack):
stracks[i].covariance = cov stracks[i].covariance = cov
def activate(self, kalman_filter, frame_id): def activate(self, kalman_filter, frame_id):
"""Start a new tracklet""" """Start a new tracklet."""
self.kalman_filter = kalman_filter self.kalman_filter = kalman_filter
self.track_id = self.next_id() self.track_id = self.next_id()
self.mean, self.covariance = self.kalman_filter.initiate(self.convert_coords(self._tlwh)) self.mean, self.covariance = self.kalman_filter.initiate(self.convert_coords(self._tlwh))
@ -179,7 +178,7 @@ class BYTETracker:
scores = results.conf scores = results.conf
bboxes = results.xyxy bboxes = results.xyxy
# add index # Add index
bboxes = np.concatenate([bboxes, np.arange(len(bboxes)).reshape(-1, 1)], axis=-1) bboxes = np.concatenate([bboxes, np.arange(len(bboxes)).reshape(-1, 1)], axis=-1)
cls = results.cls cls = results.cls
@ -196,7 +195,7 @@ class BYTETracker:
cls_second = cls[inds_second] cls_second = cls[inds_second]
detections = self.init_track(dets, scores_keep, cls_keep, img) detections = self.init_track(dets, scores_keep, cls_keep, img)
""" Add newly detected tracklets to tracked_stracks""" # Add newly detected tracklets to tracked_stracks
unconfirmed = [] unconfirmed = []
tracked_stracks = [] # type: list[STrack] tracked_stracks = [] # type: list[STrack]
for track in self.tracked_stracks: for track in self.tracked_stracks:
@ -204,7 +203,7 @@ class BYTETracker:
unconfirmed.append(track) unconfirmed.append(track)
else: else:
tracked_stracks.append(track) tracked_stracks.append(track)
""" Step 2: First association, with high score detection boxes""" # Step 2: First association, with high score detection boxes
strack_pool = self.joint_stracks(tracked_stracks, self.lost_stracks) strack_pool = self.joint_stracks(tracked_stracks, self.lost_stracks)
# Predict the current location with KF # Predict the current location with KF
self.multi_predict(strack_pool) self.multi_predict(strack_pool)
@ -225,7 +224,7 @@ class BYTETracker:
else: else:
track.re_activate(det, self.frame_id, new_id=False) track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track) refind_stracks.append(track)
""" Step 3: Second association, with low score detection boxes""" # Step 3: Second association, with low score detection boxes
# association the untrack to the low score detections # association the untrack to the low score detections
detections_second = self.init_track(dets_second, scores_second, cls_second, img) detections_second = self.init_track(dets_second, scores_second, cls_second, img)
r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked] r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked]
@ -247,7 +246,7 @@ class BYTETracker:
if track.state != TrackState.Lost: if track.state != TrackState.Lost:
track.mark_lost() track.mark_lost()
lost_stracks.append(track) lost_stracks.append(track)
"""Deal with unconfirmed tracks, usually tracks with only one beginning frame""" # Deal with unconfirmed tracks, usually tracks with only one beginning frame
detections = [detections[i] for i in u_detection] detections = [detections[i] for i in u_detection]
dists = self.get_dists(unconfirmed, detections) dists = self.get_dists(unconfirmed, detections)
matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7) matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7)
@ -258,14 +257,14 @@ class BYTETracker:
track = unconfirmed[it] track = unconfirmed[it]
track.mark_removed() track.mark_removed()
removed_stracks.append(track) removed_stracks.append(track)
""" Step 4: Init new stracks""" # Step 4: Init new stracks
for inew in u_detection: for inew in u_detection:
track = detections[inew] track = detections[inew]
if track.score < self.args.new_track_thresh: if track.score < self.args.new_track_thresh:
continue continue
track.activate(self.kalman_filter, self.frame_id) track.activate(self.kalman_filter, self.frame_id)
activated_starcks.append(track) activated_starcks.append(track)
""" Step 5: Update state""" # Step 5: Update state
for track in self.lost_stracks: for track in self.lost_stracks:
if self.frame_id - track.end_frame > self.max_time_lost: if self.frame_id - track.end_frame > self.max_time_lost:
track.mark_removed() track.mark_removed()
@ -320,7 +319,7 @@ class BYTETracker:
@staticmethod @staticmethod
def sub_stracks(tlista, tlistb): def sub_stracks(tlista, tlistb):
""" DEPRECATED CODE in https://github.com/ultralytics/ultralytics/pull/1890/ """DEPRECATED CODE in https://github.com/ultralytics/ultralytics/pull/1890/
stracks = {t.track_id: t for t in tlista} stracks = {t.track_id: t for t in tlista}
for t in tlistb: for t in tlistb:
tid = t.track_id tid = t.track_id

@ -83,8 +83,7 @@ class GMC:
return np.eye(2, 3) return np.eye(2, 3)
def applyEcc(self, raw_frame, detections=None): def applyEcc(self, raw_frame, detections=None):
"""Initialize."""
# Initialize
height, width, _ = raw_frame.shape height, width, _ = raw_frame.shape
frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY) frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY)
H = np.eye(2, 3, dtype=np.float32) H = np.eye(2, 3, dtype=np.float32)
@ -116,8 +115,7 @@ class GMC:
return H return H
def applyFeatures(self, raw_frame, detections=None): def applyFeatures(self, raw_frame, detections=None):
"""Initialize."""
# Initialize
height, width, _ = raw_frame.shape height, width, _ = raw_frame.shape
frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY) frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY)
H = np.eye(2, 3) H = np.eye(2, 3)
@ -129,7 +127,7 @@ class GMC:
width = width // self.downscale width = width // self.downscale
height = height // self.downscale height = height // self.downscale
# find the keypoints # Find the keypoints
mask = np.zeros_like(frame) mask = np.zeros_like(frame)
# mask[int(0.05 * height): int(0.95 * height), int(0.05 * width): int(0.95 * width)] = 255 # mask[int(0.05 * height): int(0.95 * height), int(0.05 * width): int(0.95 * width)] = 255
mask[int(0.02 * height):int(0.98 * height), int(0.02 * width):int(0.98 * width)] = 255 mask[int(0.02 * height):int(0.98 * height), int(0.02 * width):int(0.98 * width)] = 255
@ -140,7 +138,7 @@ class GMC:
keypoints = self.detector.detect(frame, mask) keypoints = self.detector.detect(frame, mask)
# compute the descriptors # Compute the descriptors
keypoints, descriptors = self.extractor.compute(frame, keypoints) keypoints, descriptors = self.extractor.compute(frame, keypoints)
# Handle first frame # Handle first frame
@ -243,7 +241,7 @@ class GMC:
return H return H
def applySparseOptFlow(self, raw_frame, detections=None): def applySparseOptFlow(self, raw_frame, detections=None):
# Initialize """Initialize."""
# t0 = time.time() # t0 = time.time()
height, width, _ = raw_frame.shape height, width, _ = raw_frame.shape
frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY) frame = cv2.cvtColor(raw_frame, cv2.COLOR_BGR2GRAY)
@ -254,7 +252,7 @@ class GMC:
# frame = cv2.GaussianBlur(frame, (3, 3), 1.5) # frame = cv2.GaussianBlur(frame, (3, 3), 1.5)
frame = cv2.resize(frame, (width // self.downscale, height // self.downscale)) frame = cv2.resize(frame, (width // self.downscale, height // self.downscale))
# find the keypoints # Find the keypoints
keypoints = cv2.goodFeaturesToTrack(frame, mask=None, **self.feature_params) keypoints = cv2.goodFeaturesToTrack(frame, mask=None, **self.feature_params)
# Handle first frame # Handle first frame
@ -268,10 +266,10 @@ class GMC:
return H return H
# find correspondences # Find correspondences
matchedKeypoints, status, err = cv2.calcOpticalFlowPyrLK(self.prevFrame, frame, self.prevKeyPoints, None) matchedKeypoints, status, err = cv2.calcOpticalFlowPyrLK(self.prevFrame, frame, self.prevKeyPoints, None)
# leave good correspondences only # Leave good correspondences only
prevPoints = [] prevPoints = []
currPoints = [] currPoints = []

@ -8,6 +8,7 @@ from .kalman_filter import chi2inv95
try: try:
import lap # for linear_assignment import lap # for linear_assignment
assert lap.__version__ # verify package is not directory assert lap.__version__ # verify package is not directory
except (ImportError, AssertionError, AttributeError): except (ImportError, AssertionError, AttributeError):
from ultralytics.yolo.utils.checks import check_requirements from ultralytics.yolo.utils.checks import check_requirements
@ -45,7 +46,7 @@ def _indices_to_matches(cost_matrix, indices, thresh):
def linear_assignment(cost_matrix, thresh, use_lap=True): def linear_assignment(cost_matrix, thresh, use_lap=True):
# Linear assignment implementations with scipy and lap.lapjv """Linear assignment implementations with scipy and lap.lapjv."""
if cost_matrix.size == 0: if cost_matrix.size == 0:
return np.empty((0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(range(cost_matrix.shape[1])) return np.empty((0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(range(cost_matrix.shape[1]))

@ -400,5 +400,5 @@ def copy_default_cfg():
if __name__ == '__main__': if __name__ == '__main__':
# entrypoint(debug='yolo predict model=yolov8n.pt') # Example Usage: entrypoint(debug='yolo predict model=yolov8n.pt')
entrypoint(debug='') entrypoint(debug='')

@ -66,7 +66,7 @@ class Compose:
class BaseMixTransform: class BaseMixTransform:
"""This implementation is from mmyolo""" """This implementation is from mmyolo."""
def __init__(self, dataset, pre_transform=None, p=0.0) -> None: def __init__(self, dataset, pre_transform=None, p=0.0) -> None:
self.dataset = dataset self.dataset = dataset
@ -77,12 +77,12 @@ class BaseMixTransform:
if random.uniform(0, 1) > self.p: if random.uniform(0, 1) > self.p:
return labels return labels
# get index of one or three other images # Get index of one or three other images
indexes = self.get_indexes() indexes = self.get_indexes()
if isinstance(indexes, int): if isinstance(indexes, int):
indexes = [indexes] indexes = [indexes]
# get images information will be used for Mosaic or MixUp # Get images information will be used for Mosaic or MixUp
mix_labels = [self.dataset.get_label_info(i) for i in indexes] mix_labels = [self.dataset.get_label_info(i) for i in indexes]
if self.pre_transform is not None: if self.pre_transform is not None:
@ -132,7 +132,7 @@ class Mosaic(BaseMixTransform):
img = labels_patch['img'] img = labels_patch['img']
h, w = labels_patch.pop('resized_shape') h, w = labels_patch.pop('resized_shape')
# place img in img4 # Place img in img4
if i == 0: # top left if i == 0: # top left
img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image) x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)
@ -158,7 +158,7 @@ class Mosaic(BaseMixTransform):
return final_labels return final_labels
def _update_labels(self, labels, padw, padh): def _update_labels(self, labels, padw, padh):
"""Update labels""" """Update labels."""
nh, nw = labels['img'].shape[:2] nh, nw = labels['img'].shape[:2]
labels['instances'].convert_bbox(format='xyxy') labels['instances'].convert_bbox(format='xyxy')
labels['instances'].denormalize(nw, nh) labels['instances'].denormalize(nw, nh)
@ -193,7 +193,7 @@ class MixUp(BaseMixTransform):
return random.randint(0, len(self.dataset) - 1) return random.randint(0, len(self.dataset) - 1)
def _mix_transform(self, labels): def _mix_transform(self, labels):
# Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf """Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf."""
r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0 r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0
labels2 = labels['mix_labels'][0] labels2 = labels['mix_labels'][0]
labels['img'] = (labels['img'] * r + labels2['img'] * (1 - r)).astype(np.uint8) labels['img'] = (labels['img'] * r + labels2['img'] * (1 - r)).astype(np.uint8)
@ -217,12 +217,12 @@ class RandomPerspective:
self.scale = scale self.scale = scale
self.shear = shear self.shear = shear
self.perspective = perspective self.perspective = perspective
# mosaic border # Mosaic border
self.border = border self.border = border
self.pre_transform = pre_transform self.pre_transform = pre_transform
def affine_transform(self, img, border): def affine_transform(self, img, border):
# Center """Center."""
C = np.eye(3, dtype=np.float32) C = np.eye(3, dtype=np.float32)
C[0, 2] = -img.shape[1] / 2 # x translation (pixels) C[0, 2] = -img.shape[1] / 2 # x translation (pixels)
@ -253,7 +253,7 @@ class RandomPerspective:
# Combined rotation matrix # Combined rotation matrix
M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT
# affine image # Affine image
if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed
if self.perspective: if self.perspective:
img = cv2.warpPerspective(img, M, dsize=self.size, borderValue=(114, 114, 114)) img = cv2.warpPerspective(img, M, dsize=self.size, borderValue=(114, 114, 114))
@ -281,7 +281,7 @@ class RandomPerspective:
xy = xy @ M.T # transform xy = xy @ M.T # transform
xy = (xy[:, :2] / xy[:, 2:3] if self.perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine xy = (xy[:, :2] / xy[:, 2:3] if self.perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine
# create new boxes # Create new boxes
x = xy[:, [0, 2, 4, 6]] x = xy[:, [0, 2, 4, 6]]
y = xy[:, [1, 3, 5, 7]] y = xy[:, [1, 3, 5, 7]]
return np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1)), dtype=bboxes.dtype).reshape(4, n).T return np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1)), dtype=bboxes.dtype).reshape(4, n).T
@ -348,7 +348,7 @@ class RandomPerspective:
img = labels['img'] img = labels['img']
cls = labels['cls'] cls = labels['cls']
instances = labels.pop('instances') instances = labels.pop('instances')
# make sure the coord formats are right # Make sure the coord formats are right
instances.convert_bbox(format='xyxy') instances.convert_bbox(format='xyxy')
instances.denormalize(*img.shape[:2][::-1]) instances.denormalize(*img.shape[:2][::-1])
@ -362,19 +362,19 @@ class RandomPerspective:
segments = instances.segments segments = instances.segments
keypoints = instances.keypoints keypoints = instances.keypoints
# update bboxes if there are segments. # Update bboxes if there are segments.
if len(segments): if len(segments):
bboxes, segments = self.apply_segments(segments, M) bboxes, segments = self.apply_segments(segments, M)
if keypoints is not None: if keypoints is not None:
keypoints = self.apply_keypoints(keypoints, M) keypoints = self.apply_keypoints(keypoints, M)
new_instances = Instances(bboxes, segments, keypoints, bbox_format='xyxy', normalized=False) new_instances = Instances(bboxes, segments, keypoints, bbox_format='xyxy', normalized=False)
# clip # Clip
new_instances.clip(*self.size) new_instances.clip(*self.size)
# filter instances # Filter instances
instances.scale(scale_w=scale, scale_h=scale, bbox_only=True) instances.scale(scale_w=scale, scale_h=scale, bbox_only=True)
# make the bboxes have the same scale with new_bboxes # Make the bboxes have the same scale with new_bboxes
i = self.box_candidates(box1=instances.bboxes.T, i = self.box_candidates(box1=instances.bboxes.T,
box2=new_instances.bboxes.T, box2=new_instances.bboxes.T,
area_thr=0.01 if len(segments) else 0.10) area_thr=0.01 if len(segments) else 0.10)
@ -441,7 +441,7 @@ class RandomFlip:
if self.direction == 'horizontal' and random.random() < self.p: if self.direction == 'horizontal' and random.random() < self.p:
img = np.fliplr(img) img = np.fliplr(img)
instances.fliplr(w) instances.fliplr(w)
# for keypoints # For keypoints
if self.flip_idx is not None and instances.keypoints is not None: if self.flip_idx is not None and instances.keypoints is not None:
instances.keypoints = np.ascontiguousarray(instances.keypoints[:, self.flip_idx, :]) instances.keypoints = np.ascontiguousarray(instances.keypoints[:, self.flip_idx, :])
labels['img'] = np.ascontiguousarray(img) labels['img'] = np.ascontiguousarray(img)
@ -450,7 +450,7 @@ class RandomFlip:
class LetterBox: class LetterBox:
"""Resize image and padding for detection, instance segmentation, pose""" """Resize image and padding for detection, instance segmentation, pose."""
def __init__(self, new_shape=(640, 640), auto=False, scaleFill=False, scaleup=True, stride=32): def __init__(self, new_shape=(640, 640), auto=False, scaleFill=False, scaleup=True, stride=32):
self.new_shape = new_shape self.new_shape = new_shape
@ -505,7 +505,7 @@ class LetterBox:
return img return img
def _update_labels(self, labels, ratio, padw, padh): def _update_labels(self, labels, ratio, padw, padh):
"""Update labels""" """Update labels."""
labels['instances'].convert_bbox(format='xyxy') labels['instances'].convert_bbox(format='xyxy')
labels['instances'].denormalize(*labels['img'].shape[:2][::-1]) labels['instances'].denormalize(*labels['img'].shape[:2][::-1])
labels['instances'].scale(*ratio) labels['instances'].scale(*ratio)
@ -519,7 +519,7 @@ class CopyPaste:
self.p = p self.p = p
def __call__(self, labels): def __call__(self, labels):
# Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy) """Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy)."""
im = labels['img'] im = labels['img']
cls = labels['cls'] cls = labels['cls']
h, w = im.shape[:2] h, w = im.shape[:2]
@ -531,7 +531,7 @@ class CopyPaste:
_, w, _ = im.shape # height, width, channels _, w, _ = im.shape # height, width, channels
im_new = np.zeros(im.shape, np.uint8) im_new = np.zeros(im.shape, np.uint8)
# calculate ioa first then select indexes randomly # Calculate ioa first then select indexes randomly
ins_flip = deepcopy(instances) ins_flip = deepcopy(instances)
ins_flip.fliplr(w) ins_flip.fliplr(w)
@ -641,7 +641,7 @@ class Format:
labels['bboxes'] = torch.from_numpy(instances.bboxes) if nl else torch.zeros((nl, 4)) labels['bboxes'] = torch.from_numpy(instances.bboxes) if nl else torch.zeros((nl, 4))
if self.return_keypoint: if self.return_keypoint:
labels['keypoints'] = torch.from_numpy(instances.keypoints) labels['keypoints'] = torch.from_numpy(instances.keypoints)
# then we can use collate_fn # Then we can use collate_fn
if self.batch_idx: if self.batch_idx:
labels['batch_idx'] = torch.zeros(nl) labels['batch_idx'] = torch.zeros(nl)
return labels return labels
@ -654,7 +654,7 @@ class Format:
return img return img
def _format_segments(self, instances, cls, w, h): def _format_segments(self, instances, cls, w, h):
"""convert polygon points to bitmap""" """convert polygon points to bitmap."""
segments = instances.segments segments = instances.segments
if self.mask_overlap: if self.mask_overlap:
masks, sorted_idx = polygons2masks_overlap((h, w), segments, downsample_ratio=self.mask_ratio) masks, sorted_idx = polygons2masks_overlap((h, w), segments, downsample_ratio=self.mask_ratio)

@ -70,7 +70,7 @@ class BaseDataset(Dataset):
self.ni = len(self.labels) self.ni = len(self.labels)
# rect stuff # Rect stuff
self.rect = rect self.rect = rect
self.batch_size = batch_size self.batch_size = batch_size
self.stride = stride self.stride = stride
@ -79,13 +79,13 @@ class BaseDataset(Dataset):
assert self.batch_size is not None assert self.batch_size is not None
self.set_rectangle() self.set_rectangle()
# cache stuff # Cache stuff
self.ims = [None] * self.ni self.ims = [None] * self.ni
self.npy_files = [Path(f).with_suffix('.npy') for f in self.im_files] self.npy_files = [Path(f).with_suffix('.npy') for f in self.im_files]
if cache: if cache:
self.cache_images(cache) self.cache_images(cache)
# transforms # Transforms
self.transforms = self.build_transforms(hyp=hyp) self.transforms = self.build_transforms(hyp=hyp)
def get_img_files(self, img_path): def get_img_files(self, img_path):
@ -96,13 +96,13 @@ class BaseDataset(Dataset):
p = Path(p) # os-agnostic p = Path(p) # os-agnostic
if p.is_dir(): # dir if p.is_dir(): # dir
f += glob.glob(str(p / '**' / '*.*'), recursive=True) f += glob.glob(str(p / '**' / '*.*'), recursive=True)
# f = list(p.rglob('*.*')) # pathlib # F = list(p.rglob('*.*')) # pathlib
elif p.is_file(): # file elif p.is_file(): # file
with open(p) as t: with open(p) as t:
t = t.read().strip().splitlines() t = t.read().strip().splitlines()
parent = str(p.parent) + os.sep parent = str(p.parent) + os.sep
f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path 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) # F += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
else: else:
raise FileNotFoundError(f'{self.prefix}{p} does not exist') raise FileNotFoundError(f'{self.prefix}{p} does not exist')
im_files = sorted(x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS) im_files = sorted(x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS)
@ -113,7 +113,7 @@ class BaseDataset(Dataset):
return im_files return im_files
def update_labels(self, include_class: Optional[list]): def update_labels(self, include_class: Optional[list]):
"""include_class, filter labels to include only these classes (optional)""" """include_class, filter labels to include only these classes (optional)."""
include_class_array = np.array(include_class).reshape(1, -1) include_class_array = np.array(include_class).reshape(1, -1)
for i in range(len(self.labels)): for i in range(len(self.labels)):
if include_class is not None: if include_class is not None:
@ -129,7 +129,7 @@ class BaseDataset(Dataset):
self.labels[i]['cls'][:, 0] = 0 self.labels[i]['cls'][:, 0] = 0
def load_image(self, i): def load_image(self, i):
# Loads 1 image from dataset index 'i', returns (im, resized hw) """Loads 1 image from dataset index 'i', returns (im, resized hw)."""
im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i] im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i]
if im is None: # not cached in RAM if im is None: # not cached in RAM
if fn.exists(): # load npy if fn.exists(): # load npy
@ -147,7 +147,7 @@ class BaseDataset(Dataset):
return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized
def cache_images(self, cache): def cache_images(self, cache):
# cache images to memory or disk """Cache images to memory or disk."""
gb = 0 # Gigabytes of cached images gb = 0 # Gigabytes of cached images
self.im_hw0, self.im_hw = [None] * self.ni, [None] * self.ni self.im_hw0, self.im_hw = [None] * self.ni, [None] * self.ni
fcn = self.cache_images_to_disk if cache == 'disk' else self.load_image fcn = self.cache_images_to_disk if cache == 'disk' else self.load_image
@ -164,7 +164,7 @@ class BaseDataset(Dataset):
pbar.close() pbar.close()
def cache_images_to_disk(self, i): def cache_images_to_disk(self, i):
# Saves an image as an *.npy file for faster loading """Saves an image as an *.npy file for faster loading."""
f = self.npy_files[i] f = self.npy_files[i]
if not f.exists(): if not f.exists():
np.save(f.as_posix(), cv2.imread(self.im_files[i])) np.save(f.as_posix(), cv2.imread(self.im_files[i]))
@ -211,17 +211,17 @@ class BaseDataset(Dataset):
return len(self.labels) return len(self.labels)
def update_labels_info(self, label): def update_labels_info(self, label):
"""custom your label format here""" """custom your label format here."""
return label return label
def build_transforms(self, hyp=None): def build_transforms(self, hyp=None):
"""Users can custom augmentations here """Users can custom augmentations here
like: like:
if self.augment: if self.augment:
# training transforms # Training transforms
return Compose([]) return Compose([])
else: else:
# val transforms # Val transforms
return Compose([]) return Compose([])
""" """
raise NotImplementedError raise NotImplementedError

@ -104,7 +104,7 @@ def build_dataloader(cfg, batch, img_path, data_info, stride=32, rect=False, ran
generator=generator), dataset generator=generator), dataset
# build classification # Build classification
# TODO: using cfg like `build_dataloader` # TODO: using cfg like `build_dataloader`
def build_classification_dataloader(path, def build_classification_dataloader(path,
imgsz=224, imgsz=224,
@ -114,7 +114,7 @@ def build_classification_dataloader(path,
rank=-1, rank=-1,
workers=8, workers=8,
shuffle=True): shuffle=True):
# Returns Dataloader object to be used with YOLOv5 Classifier """Returns Dataloader object to be used with YOLOv5 Classifier."""
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
dataset = ClassificationDataset(root=path, imgsz=imgsz, augment=augment, cache=cache) dataset = ClassificationDataset(root=path, imgsz=imgsz, augment=augment, cache=cache)
batch_size = min(batch_size, len(dataset)) batch_size = min(batch_size, len(dataset))

@ -70,7 +70,7 @@ class LoadStreams:
self.threads[i].start() self.threads[i].start()
LOGGER.info('') # newline LOGGER.info('') # newline
# check for common shapes # Check for common shapes
s = np.stack([LetterBox(imgsz, auto, stride=stride)(image=x).shape for x in self.imgs]) s = np.stack([LetterBox(imgsz, auto, stride=stride)(image=x).shape for x in self.imgs])
self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal
self.auto = auto and self.rect self.auto = auto and self.rect
@ -81,7 +81,7 @@ class LoadStreams:
LOGGER.warning('WARNING ⚠️ Stream shapes differ. For optimal performance supply similarly-shaped streams.') LOGGER.warning('WARNING ⚠️ Stream shapes differ. For optimal performance supply similarly-shaped streams.')
def update(self, i, cap, stream): def update(self, i, cap, stream):
# Read stream `i` frames in daemon thread """Read stream `i` frames in daemon thread."""
n, f = 0, self.frames[i] # frame number, frame array n, f = 0, self.frames[i] # frame number, frame array
while cap.isOpened() and n < f: while cap.isOpened() and n < f:
n += 1 n += 1
@ -123,7 +123,7 @@ class LoadStreams:
class LoadScreenshots: class LoadScreenshots:
# YOLOv8 screenshot dataloader, i.e. `yolo predict source=screen` # YOLOv8 screenshot dataloader, i.e. `yolo predict source=screen`
def __init__(self, source, imgsz=640, stride=32, auto=True, transforms=None): def __init__(self, source, imgsz=640, stride=32, auto=True, transforms=None):
# source = [screen_number left top width height] (pixels) """source = [screen_number left top width height] (pixels)."""
check_requirements('mss') check_requirements('mss')
import mss # noqa import mss # noqa
@ -156,7 +156,7 @@ class LoadScreenshots:
return self return self
def __next__(self): def __next__(self):
# mss screen capture: get raw pixels from the screen as np array """mss screen capture: get raw pixels from the screen as np array."""
im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR
s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: ' s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: '
@ -256,7 +256,7 @@ class LoadImages:
return path, im, im0, self.cap, s return path, im, im0, self.cap, s
def _new_video(self, path): def _new_video(self, path):
# Create a new video capture object """Create a new video capture object."""
self.frame = 0 self.frame = 0
self.cap = cv2.VideoCapture(path) self.cap = cv2.VideoCapture(path)
self.frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT) / self.vid_stride) self.frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT) / self.vid_stride)
@ -266,7 +266,7 @@ class LoadImages:
# self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0) # self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0)
def _cv2_rotate(self, im): def _cv2_rotate(self, im):
# Rotate a cv2 video manually """Rotate a cv2 video manually."""
if self.orientation == 0: if self.orientation == 0:
return cv2.rotate(im, cv2.ROTATE_90_CLOCKWISE) return cv2.rotate(im, cv2.ROTATE_90_CLOCKWISE)
elif self.orientation == 180: elif self.orientation == 180:
@ -291,7 +291,7 @@ class LoadPilAndNumpy:
self.auto = auto self.auto = auto
self.transforms = transforms self.transforms = transforms
self.mode = 'image' self.mode = 'image'
# generate fake paths # Generate fake paths
self.bs = len(self.im0) self.bs = len(self.im0)
@staticmethod @staticmethod

@ -55,19 +55,19 @@ class Albumentations:
def normalize(x, mean=IMAGENET_MEAN, std=IMAGENET_STD, inplace=False): def normalize(x, mean=IMAGENET_MEAN, std=IMAGENET_STD, inplace=False):
# Denormalize RGB images x per ImageNet stats in BCHW format, i.e. = (x - mean) / std """Denormalize RGB images x per ImageNet stats in BCHW format, i.e. = (x - mean) / std."""
return TF.normalize(x, mean, std, inplace=inplace) return TF.normalize(x, mean, std, inplace=inplace)
def denormalize(x, mean=IMAGENET_MEAN, std=IMAGENET_STD): def denormalize(x, mean=IMAGENET_MEAN, std=IMAGENET_STD):
# Denormalize RGB images x per ImageNet stats in BCHW format, i.e. = x * std + mean """Denormalize RGB images x per ImageNet stats in BCHW format, i.e. = x * std + mean."""
for i in range(3): for i in range(3):
x[:, i] = x[:, i] * std[i] + mean[i] x[:, i] = x[:, i] * std[i] + mean[i]
return x return x
def augment_hsv(im, hgain=0.5, sgain=0.5, vgain=0.5): def augment_hsv(im, hgain=0.5, sgain=0.5, vgain=0.5):
# HSV color-space augmentation """HSV color-space augmentation."""
if hgain or sgain or vgain: if hgain or sgain or vgain:
r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1 # random gains r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1 # random gains
hue, sat, val = cv2.split(cv2.cvtColor(im, cv2.COLOR_BGR2HSV)) hue, sat, val = cv2.split(cv2.cvtColor(im, cv2.COLOR_BGR2HSV))
@ -83,7 +83,7 @@ def augment_hsv(im, hgain=0.5, sgain=0.5, vgain=0.5):
def hist_equalize(im, clahe=True, bgr=False): def hist_equalize(im, clahe=True, bgr=False):
# Equalize histogram on BGR image 'im' with im.shape(n,m,3) and range 0-255 """Equalize histogram on BGR image 'im' with im.shape(n,m,3) and range 0-255."""
yuv = cv2.cvtColor(im, cv2.COLOR_BGR2YUV if bgr else cv2.COLOR_RGB2YUV) yuv = cv2.cvtColor(im, cv2.COLOR_BGR2YUV if bgr else cv2.COLOR_RGB2YUV)
if clahe: if clahe:
c = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) c = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
@ -94,7 +94,7 @@ def hist_equalize(im, clahe=True, bgr=False):
def replicate(im, labels): def replicate(im, labels):
# Replicate labels """Replicate labels."""
h, w = im.shape[:2] h, w = im.shape[:2]
boxes = labels[:, 1:].astype(int) boxes = labels[:, 1:].astype(int)
x1, y1, x2, y2 = boxes.T x1, y1, x2, y2 = boxes.T
@ -213,7 +213,7 @@ def random_perspective(im,
xy = xy @ M.T # transform xy = xy @ M.T # transform
xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine
# clip # Clip
new[i] = segment2box(xy, width, height) new[i] = segment2box(xy, width, height)
else: # warp boxes else: # warp boxes
@ -222,16 +222,16 @@ def random_perspective(im,
xy = xy @ M.T # transform xy = xy @ M.T # transform
xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine
# create new boxes # Create new boxes
x = xy[:, [0, 2, 4, 6]] x = xy[:, [0, 2, 4, 6]]
y = xy[:, [1, 3, 5, 7]] 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 new = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T
# clip # Clip
new[:, [0, 2]] = new[:, [0, 2]].clip(0, width) new[:, [0, 2]] = new[:, [0, 2]].clip(0, width)
new[:, [1, 3]] = new[:, [1, 3]].clip(0, height) new[:, [1, 3]] = new[:, [1, 3]].clip(0, height)
# filter candidates # Filter candidates
i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10) 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 = targets[i]
targets[:, 1:5] = new[i] targets[:, 1:5] = new[i]
@ -240,13 +240,13 @@ def random_perspective(im,
def copy_paste(im, labels, segments, p=0.5): def copy_paste(im, labels, segments, p=0.5):
# Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy) """Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy)."""
n = len(segments) n = len(segments)
if p and n: if p and n:
h, w, c = im.shape # height, width, channels h, w, c = im.shape # height, width, channels
im_new = np.zeros(im.shape, np.uint8) im_new = np.zeros(im.shape, np.uint8)
# calculate ioa first then select indexes randomly # Calculate ioa first then select indexes randomly
boxes = np.stack([w - labels[:, 3], labels[:, 2], w - labels[:, 1], labels[:, 4]], axis=-1) # (n, 4) boxes = np.stack([w - labels[:, 3], labels[:, 2], w - labels[:, 1], labels[:, 4]], axis=-1) # (n, 4)
ioa = bbox_ioa(boxes, labels[:, 1:5]) # intersection over area ioa = bbox_ioa(boxes, labels[:, 1:5]) # intersection over area
indexes = np.nonzero((ioa < 0.30).all(1))[0] # (N, ) indexes = np.nonzero((ioa < 0.30).all(1))[0] # (N, )
@ -265,7 +265,7 @@ def copy_paste(im, labels, segments, p=0.5):
def cutout(im, labels, p=0.5): def cutout(im, labels, p=0.5):
# Applies image cutout augmentation https://arxiv.org/abs/1708.04552 """Applies image cutout augmentation https://arxiv.org/abs/1708.04552."""
if random.random() < p: if random.random() < p:
h, w = im.shape[:2] h, w = im.shape[:2]
scales = [0.5] * 1 + [0.25] * 2 + [0.125] * 4 + [0.0625] * 8 + [0.03125] * 16 # image size fraction scales = [0.5] * 1 + [0.25] * 2 + [0.125] * 4 + [0.0625] * 8 + [0.03125] * 16 # image size fraction
@ -273,16 +273,16 @@ def cutout(im, labels, p=0.5):
mask_h = random.randint(1, int(h * s)) # create random masks mask_h = random.randint(1, int(h * s)) # create random masks
mask_w = random.randint(1, int(w * s)) mask_w = random.randint(1, int(w * s))
# box # Box
xmin = max(0, random.randint(0, w) - mask_w // 2) xmin = max(0, random.randint(0, w) - mask_w // 2)
ymin = max(0, random.randint(0, h) - mask_h // 2) ymin = max(0, random.randint(0, h) - mask_h // 2)
xmax = min(w, xmin + mask_w) xmax = min(w, xmin + mask_w)
ymax = min(h, ymin + mask_h) ymax = min(h, ymin + mask_h)
# apply random color mask # Apply random color mask
im[ymin:ymax, xmin:xmax] = [random.randint(64, 191) for _ in range(3)] im[ymin:ymax, xmin:xmax] = [random.randint(64, 191) for _ in range(3)]
# return unobscured labels # Return unobscured labels
if len(labels) and s > 0.03: if len(labels) and s > 0.03:
box = np.array([[xmin, ymin, xmax, ymax]], dtype=np.float32) box = np.array([[xmin, ymin, xmax, ymax]], dtype=np.float32)
ioa = bbox_ioa(box, xywhn2xyxy(labels[:, 1:5], w, h))[0] # intersection over area ioa = bbox_ioa(box, xywhn2xyxy(labels[:, 1:5], w, h))[0] # intersection over area
@ -292,7 +292,7 @@ def cutout(im, labels, p=0.5):
def mixup(im, labels, im2, labels2): def mixup(im, labels, im2, labels2):
# Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf """Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf."""
r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0 r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0
im = (im * r + im2 * (1 - r)).astype(np.uint8) im = (im * r + im2 * (1 - r)).astype(np.uint8)
labels = np.concatenate((labels, labels2), 0) labels = np.concatenate((labels, labels2), 0)
@ -350,7 +350,7 @@ def classify_albumentations(
def classify_transforms(size=224): def classify_transforms(size=224):
# Transforms to apply if albumentations not installed """Transforms to apply if albumentations not installed."""
assert isinstance(size, int), f'ERROR: classify_transforms size {size} must be integer, not (list, tuple)' assert isinstance(size, int), f'ERROR: classify_transforms size {size} must be integer, not (list, tuple)'
# T.Compose([T.ToTensor(), T.Resize(size), T.CenterCrop(size), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)]) # T.Compose([T.ToTensor(), T.Resize(size), T.CenterCrop(size), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)]) return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])

@ -50,7 +50,7 @@ for orientation in ExifTags.TAGS.keys():
def get_hash(paths): def get_hash(paths):
# Returns a single hash value of a list of paths (files or dirs) """Returns a single hash value of a list of paths (files or dirs)."""
size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes
h = hashlib.sha256(str(size).encode()) # hash sizes h = hashlib.sha256(str(size).encode()) # hash sizes
h.update(''.join(paths).encode()) # hash paths h.update(''.join(paths).encode()) # hash paths
@ -58,7 +58,7 @@ def get_hash(paths):
def exif_size(img): def exif_size(img):
# Returns exif-corrected PIL size """Returns exif-corrected PIL size."""
s = img.size # (width, height) s = img.size # (width, height)
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
rotation = dict(img._getexif().items())[orientation] rotation = dict(img._getexif().items())[orientation]
@ -94,7 +94,7 @@ def exif_transpose(image):
def seed_worker(worker_id): def seed_worker(worker_id):
# Set dataloader worker seed https://pytorch.org/docs/stable/notes/randomness.html#dataloader """Set dataloader worker seed https://pytorch.org/docs/stable/notes/randomness.html#dataloader."""
worker_seed = torch.initial_seed() % 2 ** 32 worker_seed = torch.initial_seed() % 2 ** 32
np.random.seed(worker_seed) np.random.seed(worker_seed)
random.seed(worker_seed) random.seed(worker_seed)
@ -156,7 +156,7 @@ def create_dataloader(path,
class InfiniteDataLoader(dataloader.DataLoader): class InfiniteDataLoader(dataloader.DataLoader):
""" Dataloader that reuses workers """Dataloader that reuses workers
Uses same syntax as vanilla DataLoader Uses same syntax as vanilla DataLoader
""" """
@ -175,7 +175,7 @@ class InfiniteDataLoader(dataloader.DataLoader):
class _RepeatSampler: class _RepeatSampler:
""" Sampler that repeats forever """Sampler that repeats forever
Args: Args:
sampler (Dataset.sampler): The sampler to repeat. sampler (Dataset.sampler): The sampler to repeat.
@ -192,7 +192,7 @@ class _RepeatSampler:
class LoadScreenshots: class LoadScreenshots:
# YOLOv5 screenshot dataloader, i.e. `python detect.py --source "screen 0 100 100 512 256"` # YOLOv5 screenshot dataloader, i.e. `python detect.py --source "screen 0 100 100 512 256"`
def __init__(self, source, img_size=640, stride=32, auto=True, transforms=None): def __init__(self, source, img_size=640, stride=32, auto=True, transforms=None):
# source = [screen_number left top width height] (pixels) """source = [screen_number left top width height] (pixels)."""
check_requirements('mss') check_requirements('mss')
import mss import mss
@ -224,7 +224,7 @@ class LoadScreenshots:
return self return self
def __next__(self): def __next__(self):
# mss screen capture: get raw pixels from the screen as np array """mss screen capture: get raw pixels from the screen as np array."""
im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR
s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: ' s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: '
@ -320,7 +320,7 @@ class LoadImages:
return path, im, im0, self.cap, s return path, im, im0, self.cap, s
def _new_video(self, path): def _new_video(self, path):
# Create a new video capture object """Create a new video capture object."""
self.frame = 0 self.frame = 0
self.cap = cv2.VideoCapture(path) self.cap = cv2.VideoCapture(path)
self.frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT) / self.vid_stride) self.frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT) / self.vid_stride)
@ -328,7 +328,7 @@ class LoadImages:
# self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0) # disable https://github.com/ultralytics/yolov5/issues/8493 # self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0) # disable https://github.com/ultralytics/yolov5/issues/8493
def _cv2_rotate(self, im): def _cv2_rotate(self, im):
# Rotate a cv2 video manually """Rotate a cv2 video manually."""
if self.orientation == 0: if self.orientation == 0:
return cv2.rotate(im, cv2.ROTATE_90_CLOCKWISE) return cv2.rotate(im, cv2.ROTATE_90_CLOCKWISE)
elif self.orientation == 180: elif self.orientation == 180:
@ -379,7 +379,7 @@ class LoadStreams:
self.threads[i].start() self.threads[i].start()
LOGGER.info('') # newline LOGGER.info('') # newline
# check for common shapes # Check for common shapes
s = np.stack([letterbox(x, img_size, stride=stride, auto=auto)[0].shape for x in self.imgs]) s = np.stack([letterbox(x, img_size, stride=stride, auto=auto)[0].shape for x in self.imgs])
self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal
self.auto = auto and self.rect self.auto = auto and self.rect
@ -388,7 +388,7 @@ class LoadStreams:
LOGGER.warning('WARNING ⚠️ Stream shapes differ. For optimal performance supply similarly-shaped streams.') LOGGER.warning('WARNING ⚠️ Stream shapes differ. For optimal performance supply similarly-shaped streams.')
def update(self, i, cap, stream): def update(self, i, cap, stream):
# Read stream `i` frames in daemon thread """Read stream `i` frames in daemon thread."""
n, f = 0, self.frames[i] # frame number, frame array n, f = 0, self.frames[i] # frame number, frame array
while cap.isOpened() and n < f: while cap.isOpened() and n < f:
n += 1 n += 1
@ -428,13 +428,13 @@ class LoadStreams:
def img2label_paths(img_paths): def img2label_paths(img_paths):
# Define label paths as a function of image paths """Define label paths as a function of image paths."""
sa, sb = f'{os.sep}images{os.sep}', f'{os.sep}labels{os.sep}' # /images/, /labels/ substrings sa, sb = f'{os.sep}images{os.sep}', f'{os.sep}labels{os.sep}' # /images/, /labels/ substrings
return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths] return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths]
class LoadImagesAndLabels(Dataset): class LoadImagesAndLabels(Dataset):
# YOLOv5 train_loader/val_loader, loads images and labels for training and validation """YOLOv5 train_loader/val_loader, loads images and labels for training and validation."""
cache_version = 0.6 # dataset labels *.cache version cache_version = 0.6 # dataset labels *.cache version
rand_interp_methods = [cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_LANCZOS4] rand_interp_methods = [cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_LANCZOS4]
@ -590,7 +590,7 @@ class LoadImagesAndLabels(Dataset):
pbar.close() pbar.close()
def check_cache_ram(self, safety_margin=0.1, prefix=''): def check_cache_ram(self, safety_margin=0.1, prefix=''):
# Check image caching requirements vs available memory """Check image caching requirements vs available memory."""
b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
n = min(self.n, 30) # extrapolate from 30 random images n = min(self.n, 30) # extrapolate from 30 random images
for _ in range(n): for _ in range(n):
@ -648,12 +648,6 @@ class LoadImagesAndLabels(Dataset):
def __len__(self): def __len__(self):
return len(self.im_files) return len(self.im_files)
# def __iter__(self):
# self.count = -1
# print('ran dataset iter')
# #self.shuffled_vector = np.random.permutation(self.nF) if self.augment else np.arange(self.nF)
# return self
def __getitem__(self, index): def __getitem__(self, index):
index = self.indices[index] # linear, shuffled, or image_weights index = self.indices[index] # linear, shuffled, or image_weights
@ -729,7 +723,7 @@ class LoadImagesAndLabels(Dataset):
return torch.from_numpy(img), labels_out, self.im_files[index], shapes return torch.from_numpy(img), labels_out, self.im_files[index], shapes
def load_image(self, i): def load_image(self, i):
# Loads 1 image from dataset index 'i', returns (im, original hw, resized hw) """Loads 1 image from dataset index 'i', returns (im, original hw, resized hw)."""
im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i], im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i],
if im is None: # not cached in RAM if im is None: # not cached in RAM
if fn.exists(): # load npy if fn.exists(): # load npy
@ -746,13 +740,13 @@ class LoadImagesAndLabels(Dataset):
return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized
def cache_images_to_disk(self, i): def cache_images_to_disk(self, i):
# Saves an image as an *.npy file for faster loading """Saves an image as an *.npy file for faster loading."""
f = self.npy_files[i] f = self.npy_files[i]
if not f.exists(): if not f.exists():
np.save(f.as_posix(), cv2.imread(self.im_files[i])) np.save(f.as_posix(), cv2.imread(self.im_files[i]))
def load_mosaic(self, index): def load_mosaic(self, index):
# YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic """YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic."""
labels4, segments4 = [], [] labels4, segments4 = [], []
s = self.img_size s = self.img_size
yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y
@ -762,7 +756,7 @@ class LoadImagesAndLabels(Dataset):
# Load image # Load image
img, _, (h, w) = self.load_image(index) img, _, (h, w) = self.load_image(index)
# place img in img4 # Place img in img4
if i == 0: # top left if i == 0: # top left
img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image) x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)
@ -810,7 +804,7 @@ class LoadImagesAndLabels(Dataset):
return img4, labels4 return img4, labels4
def load_mosaic9(self, index): def load_mosaic9(self, index):
# YOLOv5 9-mosaic loader. Loads 1 image + 8 random images into a 9-image mosaic """YOLOv5 9-mosaic loader. Loads 1 image + 8 random images into a 9-image mosaic."""
labels9, segments9 = [], [] labels9, segments9 = [], []
s = self.img_size s = self.img_size
indices = [index] + random.choices(self.indices, k=8) # 8 additional image indices indices = [index] + random.choices(self.indices, k=8) # 8 additional image indices
@ -820,7 +814,7 @@ class LoadImagesAndLabels(Dataset):
# Load image # Load image
img, _, (h, w) = self.load_image(index) img, _, (h, w) = self.load_image(index)
# place img in img9 # Place img in img9
if i == 0: # center if i == 0: # center
img9 = np.full((s * 3, s * 3, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles img9 = np.full((s * 3, s * 3, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
h0, w0 = h, w h0, w0 = h, w
@ -888,7 +882,7 @@ class LoadImagesAndLabels(Dataset):
@staticmethod @staticmethod
def collate_fn(batch): def collate_fn(batch):
# YOLOv8 collate function, outputs dict """YOLOv8 collate function, outputs dict."""
im, label, path, shapes = zip(*batch) # transposed im, label, path, shapes = zip(*batch) # transposed
for i, lb in enumerate(label): for i, lb in enumerate(label):
lb[:, 0] = i # add target image index for build_targets() lb[:, 0] = i # add target image index for build_targets()
@ -904,7 +898,7 @@ class LoadImagesAndLabels(Dataset):
@staticmethod @staticmethod
def collate_fn_old(batch): def collate_fn_old(batch):
# YOLOv5 original collate function """YOLOv5 original collate function."""
im, label, path, shapes = zip(*batch) # transposed im, label, path, shapes = zip(*batch) # transposed
for i, lb in enumerate(label): for i, lb in enumerate(label):
lb[:, 0] = i # add target image index for build_targets() lb[:, 0] = i # add target image index for build_targets()
@ -913,7 +907,7 @@ class LoadImagesAndLabels(Dataset):
# Ancillary functions -------------------------------------------------------------------------------------------------- # Ancillary functions --------------------------------------------------------------------------------------------------
def flatten_recursive(path=DATASETS_DIR / 'coco128'): def flatten_recursive(path=DATASETS_DIR / 'coco128'):
# Flatten a recursive directory by bringing all files to top level """Flatten a recursive directory by bringing all files to top level."""
new_path = Path(f'{str(path)}_flat') new_path = Path(f'{str(path)}_flat')
if os.path.exists(new_path): if os.path.exists(new_path):
shutil.rmtree(new_path) # delete output folder shutil.rmtree(new_path) # delete output folder
@ -930,11 +924,11 @@ def extract_boxes(path=DATASETS_DIR / 'coco128'): # from utils.dataloaders impo
n = len(files) # number of files n = len(files) # number of files
for im_file in tqdm(files, total=n): for im_file in tqdm(files, total=n):
if im_file.suffix[1:] in IMG_FORMATS: if im_file.suffix[1:] in IMG_FORMATS:
# image # Image
im = cv2.imread(str(im_file))[..., ::-1] # BGR to RGB im = cv2.imread(str(im_file))[..., ::-1] # BGR to RGB
h, w = im.shape[:2] h, w = im.shape[:2]
# labels # Labels
lb_file = Path(img2label_paths([str(im_file)])[0]) lb_file = Path(img2label_paths([str(im_file)])[0])
if Path(lb_file).exists(): if Path(lb_file).exists():
with open(lb_file) as f: with open(lb_file) as f:
@ -947,7 +941,7 @@ def extract_boxes(path=DATASETS_DIR / 'coco128'): # from utils.dataloaders impo
f.parent.mkdir(parents=True) f.parent.mkdir(parents=True)
b = x[1:] * [w, h, w, h] # box b = x[1:] * [w, h, w, h] # box
# b[2:] = b[2:].max() # rectangle to square # B[2:] = b[2:].max() # rectangle to square
b[2:] = b[2:] * 1.2 + 3 # pad b[2:] = b[2:] * 1.2 + 3 # pad
b = xywh2xyxy(b.reshape(-1, 4)).ravel().astype(int) b = xywh2xyxy(b.reshape(-1, 4)).ravel().astype(int)
@ -957,7 +951,7 @@ def extract_boxes(path=DATASETS_DIR / 'coco128'): # from utils.dataloaders impo
def autosplit(path=DATASETS_DIR / 'coco128/images', weights=(0.9, 0.1, 0.0), annotated_only=False): def autosplit(path=DATASETS_DIR / 'coco128/images', weights=(0.9, 0.1, 0.0), annotated_only=False):
""" Autosplit a dataset into train/val/test splits and save path/autosplit_*.txt files """Autosplit a dataset into train/val/test splits and save path/autosplit_*.txt files
Usage: from utils.dataloaders import *; autosplit() Usage: from utils.dataloaders import *; autosplit()
Arguments Arguments
path: Path to images directory path: Path to images directory
@ -983,11 +977,11 @@ def autosplit(path=DATASETS_DIR / 'coco128/images', weights=(0.9, 0.1, 0.0), ann
def verify_image_label(args): def verify_image_label(args):
# Verify one image-label pair """Verify one image-label pair."""
im_file, lb_file, prefix = args im_file, lb_file, prefix = args
nm, nf, ne, nc, msg, segments = 0, 0, 0, 0, '', [] # number (missing, found, empty, corrupt), message, segments nm, nf, ne, nc, msg, segments = 0, 0, 0, 0, '', [] # number (missing, found, empty, corrupt), message, segments
try: try:
# verify images # Verify images
im = Image.open(im_file) im = Image.open(im_file)
im.verify() # PIL verify im.verify() # PIL verify
shape = exif_size(im) # image size shape = exif_size(im) # image size
@ -1000,7 +994,7 @@ def verify_image_label(args):
ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100) ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100)
msg = f'{prefix}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved' msg = f'{prefix}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved'
# verify labels # Verify labels
if os.path.isfile(lb_file): if os.path.isfile(lb_file):
nf = 1 # label found nf = 1 # label found
with open(lb_file) as f: with open(lb_file) as f:
@ -1077,7 +1071,7 @@ def create_classification_dataloader(path,
rank=-1, rank=-1,
workers=8, workers=8,
shuffle=True): shuffle=True):
# Returns Dataloader object to be used with YOLOv5 Classifier """Returns Dataloader object to be used with YOLOv5 Classifier."""
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
dataset = ClassificationDataset(root=path, imgsz=imgsz, augment=augment, cache=cache) dataset = ClassificationDataset(root=path, imgsz=imgsz, augment=augment, cache=cache)
batch_size = min(batch_size, len(dataset)) batch_size = min(batch_size, len(dataset))

@ -193,7 +193,7 @@ class YOLODataset(BaseDataset):
self.transforms = self.build_transforms(hyp) self.transforms = self.build_transforms(hyp)
def update_labels_info(self, label): def update_labels_info(self, label):
"""custom your label format here""" """custom your label format here."""
# NOTE: cls is not with bboxes now, classification and semantic segmentation need an independent cls label # NOTE: cls is not with bboxes now, classification and semantic segmentation need an independent cls label
# we can make it also support classification and semantic segmentation by add or remove some dict keys there. # we can make it also support classification and semantic segmentation by add or remove some dict keys there.
bboxes = label.pop('bboxes') bboxes = label.pop('bboxes')

@ -39,7 +39,7 @@ class MixAndRectDataset:
""" """
labels = deepcopy(self.dataset[index]) labels = deepcopy(self.dataset[index])
for transform in self.dataset.transforms.tolist(): for transform in self.dataset.transforms.tolist():
# mosaic and mixup # Mosaic and mixup
if hasattr(transform, 'get_indexes'): if hasattr(transform, 'get_indexes'):
indexes = transform.get_indexes(self.dataset) indexes = transform.get_indexes(self.dataset)
if not isinstance(indexes, collections.abc.Sequence): if not isinstance(indexes, collections.abc.Sequence):

@ -37,13 +37,13 @@ for orientation in ExifTags.TAGS.keys():
def img2label_paths(img_paths): def img2label_paths(img_paths):
# Define label paths as a function of image paths """Define label paths as a function of image paths."""
sa, sb = f'{os.sep}images{os.sep}', f'{os.sep}labels{os.sep}' # /images/, /labels/ substrings sa, sb = f'{os.sep}images{os.sep}', f'{os.sep}labels{os.sep}' # /images/, /labels/ substrings
return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths] return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths]
def get_hash(paths): def get_hash(paths):
# Returns a single hash value of a list of paths (files or dirs) """Returns a single hash value of a list of paths (files or dirs)."""
size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes
h = hashlib.sha256(str(size).encode()) # hash sizes h = hashlib.sha256(str(size).encode()) # hash sizes
h.update(''.join(paths).encode()) # hash paths h.update(''.join(paths).encode()) # hash paths
@ -51,7 +51,7 @@ def get_hash(paths):
def exif_size(img): def exif_size(img):
# Returns exif-corrected PIL size """Returns exif-corrected PIL size."""
s = img.size # (width, height) s = img.size # (width, height)
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
rotation = dict(img._getexif().items())[orientation] rotation = dict(img._getexif().items())[orientation]
@ -61,12 +61,12 @@ def exif_size(img):
def verify_image_label(args): def verify_image_label(args):
# Verify one image-label pair """Verify one image-label pair."""
im_file, lb_file, prefix, keypoint, num_cls, nkpt, ndim = args im_file, lb_file, prefix, keypoint, num_cls, nkpt, ndim = args
# number (missing, found, empty, corrupt), message, segments, keypoints # Number (missing, found, empty, corrupt), message, segments, keypoints
nm, nf, ne, nc, msg, segments, keypoints = 0, 0, 0, 0, '', [], None nm, nf, ne, nc, msg, segments, keypoints = 0, 0, 0, 0, '', [], None
try: try:
# verify images # Verify images
im = Image.open(im_file) im = Image.open(im_file)
im.verify() # PIL verify im.verify() # PIL verify
shape = exif_size(im) # image size shape = exif_size(im) # image size
@ -80,7 +80,7 @@ def verify_image_label(args):
ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100) ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100)
msg = f'{prefix}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved' msg = f'{prefix}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved'
# verify labels # Verify labels
if os.path.isfile(lb_file): if os.path.isfile(lb_file):
nf = 1 # label found nf = 1 # label found
with open(lb_file) as f: with open(lb_file) as f:
@ -191,7 +191,7 @@ def polygons2masks_overlap(imgsz, segments, downsample_ratio=1):
def check_det_dataset(dataset, autodownload=True): def check_det_dataset(dataset, autodownload=True):
# Download, check and/or unzip dataset if not found locally """Download, check and/or unzip dataset if not found locally."""
data = check_file(dataset) data = check_file(dataset)
# Download (optional) # Download (optional)
@ -321,7 +321,7 @@ class HUBDatasetStats():
""" """
def __init__(self, path='coco128.yaml', autodownload=False): def __init__(self, path='coco128.yaml', autodownload=False):
# Initialize class """Initialize class."""
zipped, data_dir, yaml_path = self._unzip(Path(path)) zipped, data_dir, yaml_path = self._unzip(Path(path))
try: try:
# data = yaml_load(check_yaml(yaml_path)) # data dict # data = yaml_load(check_yaml(yaml_path)) # data dict
@ -339,7 +339,7 @@ class HUBDatasetStats():
@staticmethod @staticmethod
def _find_yaml(dir): def _find_yaml(dir):
# Return data.yaml file """Return data.yaml file."""
files = list(dir.glob('*.yaml')) or list(dir.rglob('*.yaml')) # try root level first and then recursive files = list(dir.glob('*.yaml')) or list(dir.rglob('*.yaml')) # try root level first and then recursive
assert files, f'No *.yaml file found in {dir}' assert files, f'No *.yaml file found in {dir}'
if len(files) > 1: if len(files) > 1:
@ -349,7 +349,7 @@ class HUBDatasetStats():
return files[0] return files[0]
def _unzip(self, path): def _unzip(self, path):
# Unzip data.zip """Unzip data.zip."""
if not str(path).endswith('.zip'): # path is data.yaml if not str(path).endswith('.zip'): # path is data.yaml
return False, None, path return False, None, path
assert Path(path).is_file(), f'Error unzipping {path}, file not found' assert Path(path).is_file(), f'Error unzipping {path}, file not found'
@ -362,12 +362,12 @@ class HUBDatasetStats():
compress_one_image(f, self.im_dir / Path(f).name) # save to dataset-hub compress_one_image(f, self.im_dir / Path(f).name) # save to dataset-hub
def get_json(self, save=False, verbose=False): def get_json(self, save=False, verbose=False):
# Return dataset JSON for Ultralytics HUB """Return dataset JSON for Ultralytics HUB."""
# from ultralytics.yolo.data import YOLODataset # from ultralytics.yolo.data import YOLODataset
from ultralytics.yolo.data.dataloaders.v5loader import LoadImagesAndLabels from ultralytics.yolo.data.dataloaders.v5loader import LoadImagesAndLabels
def _round(labels): def _round(labels):
# Update labels to integer class and 6 decimal place floats """Update labels to integer class and 6 decimal place floats."""
return [[int(c), *(round(x, 4) for x in points)] for c, *points in labels] return [[int(c), *(round(x, 4) for x in points)] for c, *points in labels]
for split in 'train', 'val', 'test': for split in 'train', 'val', 'test':
@ -400,7 +400,7 @@ class HUBDatasetStats():
return self.stats return self.stats
def process_images(self): def process_images(self):
# Compress images for Ultralytics HUB """Compress images for Ultralytics HUB."""
# from ultralytics.yolo.data import YOLODataset # from ultralytics.yolo.data import YOLODataset
from ultralytics.yolo.data.dataloaders.v5loader import LoadImagesAndLabels from ultralytics.yolo.data.dataloaders.v5loader import LoadImagesAndLabels

@ -73,7 +73,7 @@ ARM64 = platform.machine() in ('arm64', 'aarch64')
def export_formats(): def export_formats():
"""YOLOv8 export formats""" """YOLOv8 export formats."""
import pandas import pandas
x = [ x = [
['PyTorch', '-', '.pt', True, True], ['PyTorch', '-', '.pt', True, True],
@ -92,7 +92,7 @@ def export_formats():
def gd_outputs(gd): def gd_outputs(gd):
"""TensorFlow GraphDef model output node names""" """TensorFlow GraphDef model output node names."""
name_list, input_list = [], [] name_list, input_list = [], []
for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef
name_list.append(node.name) name_list.append(node.name)
@ -101,7 +101,7 @@ def gd_outputs(gd):
def try_export(inner_func): def try_export(inner_func):
"""YOLOv8 export decorator, i..e @try_export""" """YOLOv8 export decorator, i..e @try_export."""
inner_args = get_default_args(inner_func) inner_args = get_default_args(inner_func)
def outer_func(*args, **kwargs): def outer_func(*args, **kwargs):
@ -119,7 +119,7 @@ def try_export(inner_func):
class iOSDetectModel(torch.nn.Module): class iOSDetectModel(torch.nn.Module):
"""Wrap an Ultralytics YOLO model for iOS export""" """Wrap an Ultralytics YOLO model for iOS export."""
def __init__(self, model, im): def __init__(self, model, im):
super().__init__() super().__init__()
@ -246,28 +246,28 @@ class Exporter:
# Exports # Exports
f = [''] * len(fmts) # exported filenames f = [''] * len(fmts) # exported filenames
if jit: # TorchScript if jit: # TorchScript
f[0], _ = self._export_torchscript() f[0], _ = self.export_torchscript()
if engine: # TensorRT required before ONNX if engine: # TensorRT required before ONNX
f[1], _ = self._export_engine() f[1], _ = self.export_engine()
if onnx or xml: # OpenVINO requires ONNX if onnx or xml: # OpenVINO requires ONNX
f[2], _ = self._export_onnx() f[2], _ = self.export_onnx()
if xml: # OpenVINO if xml: # OpenVINO
f[3], _ = self._export_openvino() f[3], _ = self.export_openvino()
if coreml: # CoreML if coreml: # CoreML
f[4], _ = self._export_coreml() f[4], _ = self.export_coreml()
if any((saved_model, pb, tflite, edgetpu, tfjs)): # TensorFlow formats if any((saved_model, pb, tflite, edgetpu, tfjs)): # TensorFlow formats
self.args.int8 |= edgetpu self.args.int8 |= edgetpu
f[5], s_model = self._export_saved_model() f[5], s_model = self.export_saved_model()
if pb or tfjs: # pb prerequisite to tfjs if pb or tfjs: # pb prerequisite to tfjs
f[6], _ = self._export_pb(s_model) f[6], _ = self.export_pb(s_model)
if tflite: if tflite:
f[7], _ = self._export_tflite(s_model, nms=False, agnostic_nms=self.args.agnostic_nms) f[7], _ = self.export_tflite(s_model, nms=False, agnostic_nms=self.args.agnostic_nms)
if edgetpu: if edgetpu:
f[8], _ = self._export_edgetpu(tflite_model=Path(f[5]) / f'{self.file.stem}_full_integer_quant.tflite') f[8], _ = self.export_edgetpu(tflite_model=Path(f[5]) / f'{self.file.stem}_full_integer_quant.tflite')
if tfjs: if tfjs:
f[9], _ = self._export_tfjs() f[9], _ = self.export_tfjs()
if paddle: # PaddlePaddle if paddle: # PaddlePaddle
f[10], _ = self._export_paddle() f[10], _ = self.export_paddle()
# Finish # Finish
f = [str(x) for x in f if x] # filter out '' and None f = [str(x) for x in f if x] # filter out '' and None
@ -289,8 +289,8 @@ class Exporter:
return f # return list of exported files/dirs return f # return list of exported files/dirs
@try_export @try_export
def _export_torchscript(self, prefix=colorstr('TorchScript:')): def export_torchscript(self, prefix=colorstr('TorchScript:')):
# YOLOv8 TorchScript model export """YOLOv8 TorchScript model export."""
LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...') LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...')
f = self.file.with_suffix('.torchscript') f = self.file.with_suffix('.torchscript')
@ -305,8 +305,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_onnx(self, prefix=colorstr('ONNX:')): def export_onnx(self, prefix=colorstr('ONNX:')):
# YOLOv8 ONNX export """YOLOv8 ONNX export."""
requirements = ['onnx>=1.12.0'] requirements = ['onnx>=1.12.0']
if self.args.simplify: if self.args.simplify:
requirements += ['onnxsim>=0.4.17', 'onnxruntime-gpu' if torch.cuda.is_available() else 'onnxruntime'] requirements += ['onnxsim>=0.4.17', 'onnxruntime-gpu' if torch.cuda.is_available() else 'onnxruntime']
@ -363,8 +363,8 @@ class Exporter:
return f, model_onnx return f, model_onnx
@try_export @try_export
def _export_openvino(self, prefix=colorstr('OpenVINO:')): def export_openvino(self, prefix=colorstr('OpenVINO:')):
# YOLOv8 OpenVINO export """YOLOv8 OpenVINO export."""
check_requirements('openvino-dev>=2022.3') # requires openvino-dev: https://pypi.org/project/openvino-dev/ check_requirements('openvino-dev>=2022.3') # requires openvino-dev: https://pypi.org/project/openvino-dev/
import openvino.runtime as ov # noqa import openvino.runtime as ov # noqa
from openvino.tools import mo # noqa from openvino.tools import mo # noqa
@ -383,8 +383,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_paddle(self, prefix=colorstr('PaddlePaddle:')): def export_paddle(self, prefix=colorstr('PaddlePaddle:')):
# YOLOv8 Paddle export """YOLOv8 Paddle export."""
check_requirements(('paddlepaddle', 'x2paddle')) check_requirements(('paddlepaddle', 'x2paddle'))
import x2paddle # noqa import x2paddle # noqa
from x2paddle.convert import pytorch2paddle # noqa from x2paddle.convert import pytorch2paddle # noqa
@ -397,8 +397,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_coreml(self, prefix=colorstr('CoreML:')): def export_coreml(self, prefix=colorstr('CoreML:')):
# YOLOv8 CoreML export """YOLOv8 CoreML export."""
check_requirements('coremltools>=6.0') check_requirements('coremltools>=6.0')
import coremltools as ct # noqa import coremltools as ct # noqa
@ -439,8 +439,8 @@ class Exporter:
return f, ct_model return f, ct_model
@try_export @try_export
def _export_engine(self, workspace=4, verbose=False, prefix=colorstr('TensorRT:')): def export_engine(self, workspace=4, verbose=False, prefix=colorstr('TensorRT:')):
# YOLOv8 TensorRT export https://developer.nvidia.com/tensorrt """YOLOv8 TensorRT export https://developer.nvidia.com/tensorrt."""
assert self.im.device.type != 'cpu', "export running on CPU but must be on GPU, i.e. use 'device=0'" assert self.im.device.type != 'cpu', "export running on CPU but must be on GPU, i.e. use 'device=0'"
try: try:
import tensorrt as trt # noqa import tensorrt as trt # noqa
@ -451,7 +451,7 @@ class Exporter:
check_version(trt.__version__, '7.0.0', hard=True) # require tensorrt>=8.0.0 check_version(trt.__version__, '7.0.0', hard=True) # require tensorrt>=8.0.0
self.args.simplify = True self.args.simplify = True
f_onnx, _ = self._export_onnx() f_onnx, _ = self.export_onnx()
LOGGER.info(f'\n{prefix} starting export with TensorRT {trt.__version__}...') LOGGER.info(f'\n{prefix} starting export with TensorRT {trt.__version__}...')
assert Path(f_onnx).exists(), f'failed to export ONNX file: {f_onnx}' assert Path(f_onnx).exists(), f'failed to export ONNX file: {f_onnx}'
@ -504,9 +504,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_saved_model(self, prefix=colorstr('TensorFlow SavedModel:')): def export_saved_model(self, prefix=colorstr('TensorFlow SavedModel:')):
"""YOLOv8 TensorFlow SavedModel export."""
# YOLOv8 TensorFlow SavedModel export
try: try:
import tensorflow as tf # noqa import tensorflow as tf # noqa
except ImportError: except ImportError:
@ -525,7 +524,7 @@ class Exporter:
# Export to ONNX # Export to ONNX
self.args.simplify = True self.args.simplify = True
f_onnx, _ = self._export_onnx() f_onnx, _ = self.export_onnx()
# Export to TF # Export to TF
int8 = '-oiqt -qt per-tensor' if self.args.int8 else '' int8 = '-oiqt -qt per-tensor' if self.args.int8 else ''
@ -551,8 +550,8 @@ class Exporter:
return str(f), keras_model return str(f), keras_model
@try_export @try_export
def _export_pb(self, keras_model, prefix=colorstr('TensorFlow GraphDef:')): def export_pb(self, keras_model, prefix=colorstr('TensorFlow GraphDef:')):
# YOLOv8 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow """YOLOv8 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow."""
import tensorflow as tf # noqa import tensorflow as tf # noqa
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 # noqa from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 # noqa
@ -567,8 +566,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_tflite(self, keras_model, nms, agnostic_nms, prefix=colorstr('TensorFlow Lite:')): def export_tflite(self, keras_model, nms, agnostic_nms, prefix=colorstr('TensorFlow Lite:')):
# YOLOv8 TensorFlow Lite export """YOLOv8 TensorFlow Lite export."""
import tensorflow as tf # noqa import tensorflow as tf # noqa
LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...') LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
@ -581,44 +580,9 @@ class Exporter:
f = saved_model / f'{self.file.stem}_float32.tflite' f = saved_model / f'{self.file.stem}_float32.tflite'
return str(f), None return str(f), None
# # OLD TFLITE EXPORT CODE BELOW -------------------------------------------------------------------------------
# batch_size, ch, *imgsz = list(self.im.shape) # BCHW
# f = str(self.file).replace(self.file.suffix, '-fp16.tflite')
#
# converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
# converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
# converter.target_spec.supported_types = [tf.float16]
# converter.optimizations = [tf.lite.Optimize.DEFAULT]
# if self.args.int8:
#
# def representative_dataset_gen(dataset, n_images=100):
# # Dataset generator for use with converter.representative_dataset, returns a generator of np arrays
# for n, (path, img, im0s, vid_cap, string) in enumerate(dataset):
# im = np.transpose(img, [1, 2, 0])
# im = np.expand_dims(im, axis=0).astype(np.float32)
# im /= 255
# yield [im]
# if n >= n_images:
# break
#
# dataset = LoadImages(check_det_dataset(self.args.data)['train'], imgsz=imgsz, auto=False)
# converter.representative_dataset = lambda: representative_dataset_gen(dataset, n_images=100)
# converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# converter.target_spec.supported_types = []
# converter.inference_input_type = tf.uint8 # or tf.int8
# converter.inference_output_type = tf.uint8 # or tf.int8
# converter.experimental_new_quantizer = True
# f = str(self.file).replace(self.file.suffix, '-int8.tflite')
# if nms or agnostic_nms:
# converter.target_spec.supported_ops.append(tf.lite.OpsSet.SELECT_TF_OPS)
#
# tflite_model = converter.convert()
# open(f, 'wb').write(tflite_model)
# return f, None
@try_export @try_export
def _export_edgetpu(self, tflite_model='', prefix=colorstr('Edge TPU:')): def export_edgetpu(self, tflite_model='', prefix=colorstr('Edge TPU:')):
# YOLOv8 Edge TPU export https://coral.ai/docs/edgetpu/models-intro/ """YOLOv8 Edge TPU export https://coral.ai/docs/edgetpu/models-intro/."""
LOGGER.warning(f'{prefix} WARNING ⚠️ Edge TPU known bug https://github.com/ultralytics/ultralytics/issues/1185') LOGGER.warning(f'{prefix} WARNING ⚠️ Edge TPU known bug https://github.com/ultralytics/ultralytics/issues/1185')
cmd = 'edgetpu_compiler --version' cmd = 'edgetpu_compiler --version'
@ -644,8 +608,8 @@ class Exporter:
return f, None return f, None
@try_export @try_export
def _export_tfjs(self, prefix=colorstr('TensorFlow.js:')): def export_tfjs(self, prefix=colorstr('TensorFlow.js:')):
# YOLOv8 TensorFlow.js export """YOLOv8 TensorFlow.js export."""
check_requirements('tensorflowjs') check_requirements('tensorflowjs')
import tensorflow as tf import tensorflow as tf
import tensorflowjs as tfjs # noqa import tensorflowjs as tfjs # noqa
@ -681,7 +645,7 @@ class Exporter:
return f, None return f, None
def _add_tflite_metadata(self, file): def _add_tflite_metadata(self, file):
# Add metadata to *.tflite models per https://www.tensorflow.org/lite/models/convert/metadata """Add metadata to *.tflite models per https://www.tensorflow.org/lite/models/convert/metadata."""
from tflite_support import flatbuffers # noqa from tflite_support import flatbuffers # noqa
from tflite_support import metadata as _metadata # noqa from tflite_support import metadata as _metadata # noqa
from tflite_support import metadata_schema_py_generated as _metadata_fb # noqa from tflite_support import metadata_schema_py_generated as _metadata_fb # noqa

@ -35,6 +35,7 @@ class YOLO:
Args: Args:
model (str, Path): Path to the model file to load or create. model (str, Path): Path to the model file to load or create.
task (Any, optional): Task type for the YOLO model. Defaults to None.
Attributes: Attributes:
predictor (Any): The predictor object. predictor (Any): The predictor object.
@ -76,7 +77,6 @@ class YOLO:
Args: Args:
model (Union[str, Path], optional): Path or name of the model to load or create. Defaults to 'yolov8n.pt'. model (Union[str, Path], optional): Path or name of the model to load or create. Defaults to 'yolov8n.pt'.
task (Any, optional): Task type for the YOLO model. Defaults to None. task (Any, optional): Task type for the YOLO model. Defaults to None.
""" """
self.callbacks = callbacks.get_default_callbacks() self.callbacks = callbacks.get_default_callbacks()
self.predictor = None # reuse predictor self.predictor = None # reuse predictor
@ -273,7 +273,7 @@ class YOLO:
@smart_inference_mode() @smart_inference_mode()
def val(self, data=None, **kwargs): def val(self, data=None, **kwargs):
""" """
Validate a model on a given dataset . Validate a model on a given dataset.
Args: Args:
data (str): The dataset to validate on. Accepts all formats accepted by yolo data (str): The dataset to validate on. Accepts all formats accepted by yolo
@ -365,7 +365,7 @@ class YOLO:
self.model = self.trainer.model self.model = self.trainer.model
self.trainer.hub_session = self.session # attach optional HUB session self.trainer.hub_session = self.session # attach optional HUB session
self.trainer.train() self.trainer.train()
# update model and cfg after training # Update model and cfg after training
if RANK in (-1, 0): if RANK in (-1, 0):
self.model, _ = attempt_load_one_weight(str(self.trainer.best)) self.model, _ = attempt_load_one_weight(str(self.trainer.best))
self.overrides = self.model.args self.overrides = self.model.args
@ -467,7 +467,7 @@ class YOLO:
@property @property
def names(self): def names(self):
""" """
Returns class names of the loaded model. Returns class names of the loaded model.
""" """
return self.model.names if hasattr(self.model, 'names') else None return self.model.names if hasattr(self.model, 'names') else None
@ -481,7 +481,7 @@ class YOLO:
@property @property
def transforms(self): def transforms(self):
""" """
Returns transform of the loaded model. Returns transform of the loaded model.
""" """
return self.model.transforms if hasattr(self.model, 'transforms') else None return self.model.transforms if hasattr(self.model, 'transforms') else None

@ -134,7 +134,7 @@ class BasePredictor:
if not self.args.retina_masks: if not self.args.retina_masks:
plot_args['im_gpu'] = im[idx] plot_args['im_gpu'] = im[idx]
self.plotted_img = result.plot(**plot_args) self.plotted_img = result.plot(**plot_args)
# write # Write
if self.args.save_txt: if self.args.save_txt:
result.save_txt(f'{self.txt_path}.txt', save_conf=self.args.save_conf) result.save_txt(f'{self.txt_path}.txt', save_conf=self.args.save_conf)
if self.args.save_crop: if self.args.save_crop:
@ -153,7 +153,7 @@ class BasePredictor:
return list(self.stream_inference(source, model)) # merge list of Result into one return list(self.stream_inference(source, model)) # merge list of Result into one
def predict_cli(self, source=None, model=None): def predict_cli(self, source=None, model=None):
# Method used for CLI prediction. It uses always generator as outputs as not required by CLI mode """Method used for CLI prediction. It uses always generator as outputs as not required by CLI mode."""
gen = self.stream_inference(source, model) gen = self.stream_inference(source, model)
for _ in gen: # running CLI inference without accumulating any outputs (do not modify) for _ in gen: # running CLI inference without accumulating any outputs (do not modify)
pass pass
@ -182,16 +182,16 @@ class BasePredictor:
if self.args.verbose: if self.args.verbose:
LOGGER.info('') LOGGER.info('')
# setup model # Setup model
if not self.model: if not self.model:
self.setup_model(model) self.setup_model(model)
# setup source every time predict is called # Setup source every time predict is called
self.setup_source(source if source is not None else self.args.source) self.setup_source(source if source is not None else self.args.source)
# check if save_dir/ label file exists # Check if save_dir/ label file exists
if self.args.save or self.args.save_txt: if self.args.save or self.args.save_txt:
(self.save_dir / 'labels' if self.args.save_txt else self.save_dir).mkdir(parents=True, exist_ok=True) (self.save_dir / 'labels' if self.args.save_txt else self.save_dir).mkdir(parents=True, exist_ok=True)
# warmup model # Warmup model
if not self.done_warmup: if not self.done_warmup:
self.model.warmup(imgsz=(1 if self.model.pt or self.model.triton else self.dataset.bs, 3, *self.imgsz)) self.model.warmup(imgsz=(1 if self.model.pt or self.model.triton else self.dataset.bs, 3, *self.imgsz))
self.done_warmup = True self.done_warmup = True
@ -204,22 +204,22 @@ class BasePredictor:
path, im, im0s, vid_cap, s = batch path, im, im0s, vid_cap, s = batch
visualize = increment_path(self.save_dir / Path(path).stem, mkdir=True) if self.args.visualize else False visualize = increment_path(self.save_dir / Path(path).stem, mkdir=True) if self.args.visualize else False
# preprocess # Preprocess
with self.dt[0]: with self.dt[0]:
im = self.preprocess(im) im = self.preprocess(im)
if len(im.shape) == 3: if len(im.shape) == 3:
im = im[None] # expand for batch dim im = im[None] # expand for batch dim
# inference # Inference
with self.dt[1]: with self.dt[1]:
preds = self.model(im, augment=self.args.augment, visualize=visualize) preds = self.model(im, augment=self.args.augment, visualize=visualize)
# postprocess # Postprocess
with self.dt[2]: with self.dt[2]:
self.results = self.postprocess(preds, im, im0s) self.results = self.postprocess(preds, im, im0s)
self.run_callbacks('on_predict_postprocess_end') self.run_callbacks('on_predict_postprocess_end')
# visualize, save, write results # Visualize, save, write results
n = len(im) n = len(im)
for i in range(n): for i in range(n):
self.results[i].speed = { self.results[i].speed = {
@ -288,7 +288,7 @@ class BasePredictor:
def save_preds(self, vid_cap, idx, save_path): def save_preds(self, vid_cap, idx, save_path):
im0 = self.plotted_img im0 = self.plotted_img
# save imgs # Save imgs
if self.dataset.mode == 'image': if self.dataset.mode == 'image':
cv2.imwrite(save_path, im0) cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' else: # 'video' or 'stream'

@ -262,12 +262,12 @@ class Results(SimpleClass):
kpts = self.keypoints kpts = self.keypoints
texts = [] texts = []
if probs is not None: if probs is not None:
# classify # Classify
n5 = min(len(self.names), 5) n5 = min(len(self.names), 5)
top5i = probs.argsort(0, descending=True)[:n5].tolist() # top 5 indices top5i = probs.argsort(0, descending=True)[:n5].tolist() # top 5 indices
[texts.append(f'{probs[j]:.2f} {self.names[j]}') for j in top5i] [texts.append(f'{probs[j]:.2f} {self.names[j]}') for j in top5i]
elif boxes: elif boxes:
# detect/segment/pose # Detect/segment/pose
for j, d in enumerate(boxes): for j, d in enumerate(boxes):
c, conf, id = int(d.cls), float(d.conf), None if d.id is None else int(d.id.item()) c, conf, id = int(d.cls), float(d.conf), None if d.id is None else int(d.id.item())
line = (c, *d.xywhn.view(-1)) line = (c, *d.xywhn.view(-1))
@ -418,7 +418,7 @@ class Masks(BaseTensor):
@property @property
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def segments(self): def segments(self):
# Segments-deprecated (normalized) """Segments-deprecated (normalized)."""
LOGGER.warning("WARNING ⚠️ 'Masks.segments' is deprecated. Use 'Masks.xyn' for segments (normalized) and " LOGGER.warning("WARNING ⚠️ 'Masks.segments' is deprecated. Use 'Masks.xyn' for segments (normalized) and "
"'Masks.xy' for segments (pixels) instead.") "'Masks.xy' for segments (pixels) instead.")
return self.xyn return self.xyn
@ -426,7 +426,7 @@ class Masks(BaseTensor):
@property @property
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def xyn(self): def xyn(self):
# Segments (normalized) """Segments (normalized)."""
return [ return [
ops.scale_coords(self.data.shape[1:], x, self.orig_shape, normalize=True) ops.scale_coords(self.data.shape[1:], x, self.orig_shape, normalize=True)
for x in ops.masks2segments(self.data)] for x in ops.masks2segments(self.data)]
@ -434,7 +434,7 @@ class Masks(BaseTensor):
@property @property
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def xy(self): def xy(self):
# Segments (pixels) """Segments (pixels)."""
return [ return [
ops.scale_coords(self.data.shape[1:], x, self.orig_shape, normalize=False) ops.scale_coords(self.data.shape[1:], x, self.orig_shape, normalize=False)
for x in ops.masks2segments(self.data)] for x in ops.masks2segments(self.data)]

@ -163,7 +163,7 @@ class BaseTrainer:
callback(self) callback(self)
def train(self): def train(self):
# Allow device='', device=None on Multi-GPU systems to default to device=0 """Allow device='', device=None on Multi-GPU systems to default to device=0."""
if isinstance(self.args.device, int) or self.args.device: # i.e. device=0 or device=[0,1,2,3] if isinstance(self.args.device, int) or self.args.device: # i.e. device=0 or device=[0,1,2,3]
world_size = torch.cuda.device_count() world_size = torch.cuda.device_count()
elif torch.cuda.is_available(): # i.e. device=None or device='' elif torch.cuda.is_available(): # i.e. device=None or device=''
@ -306,7 +306,7 @@ class BaseTrainer:
xi = [0, nw] # x interp xi = [0, nw] # x interp
self.accumulate = max(1, np.interp(ni, xi, [1, self.args.nbs / self.batch_size]).round()) self.accumulate = max(1, np.interp(ni, xi, [1, self.args.nbs / self.batch_size]).round())
for j, x in enumerate(self.optimizer.param_groups): for j, x in enumerate(self.optimizer.param_groups):
# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 # Bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
x['lr'] = np.interp( x['lr'] = np.interp(
ni, xi, [self.args.warmup_bias_lr if j == 0 else 0.0, x['initial_lr'] * self.lf(epoch)]) ni, xi, [self.args.warmup_bias_lr if j == 0 else 0.0, x['initial_lr'] * self.lf(epoch)])
if 'momentum' in x: if 'momentum' in x:
@ -631,7 +631,7 @@ def check_amp(model):
return False # AMP only used on CUDA devices return False # AMP only used on CUDA devices
def amp_allclose(m, im): def amp_allclose(m, im):
# All close FP32 vs AMP results """All close FP32 vs AMP results."""
a = m(im, device=device, verbose=False)[0].boxes.data # FP32 inference a = m(im, device=device, verbose=False)[0].boxes.data # FP32 inference
with torch.cuda.amp.autocast(True): with torch.cuda.amp.autocast(True):
b = m(im, device=device, verbose=False)[0].boxes.data # AMP inference b = m(im, device=device, verbose=False)[0].boxes.data # AMP inference

@ -149,20 +149,20 @@ class BaseValidator:
for batch_i, batch in enumerate(bar): for batch_i, batch in enumerate(bar):
self.run_callbacks('on_val_batch_start') self.run_callbacks('on_val_batch_start')
self.batch_i = batch_i self.batch_i = batch_i
# preprocess # Preprocess
with dt[0]: with dt[0]:
batch = self.preprocess(batch) batch = self.preprocess(batch)
# inference # Inference
with dt[1]: with dt[1]:
preds = model(batch['img']) preds = model(batch['img'])
# loss # Loss
with dt[2]: with dt[2]:
if self.training: if self.training:
self.loss += trainer.criterion(preds, batch)[1] self.loss += trainer.criterion(preds, batch)[1]
# postprocess # Postprocess
with dt[3]: with dt[3]:
preds = self.postprocess(preds) preds = self.postprocess(preds)

@ -199,7 +199,7 @@ def plt_settings(rcparams={'font.size': 11}, backend='Agg'):
def set_logging(name=LOGGING_NAME, verbose=True): def set_logging(name=LOGGING_NAME, verbose=True):
# sets up logging for the given name """Sets up logging for the given name."""
rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings
level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR
logging.config.dictConfig({ logging.config.dictConfig({
@ -539,12 +539,12 @@ SETTINGS_YAML = USER_CONFIG_DIR / 'settings.yaml'
def emojis(string=''): def emojis(string=''):
# Return platform-dependent emoji-safe version of string """Return platform-dependent emoji-safe version of string."""
return string.encode().decode('ascii', 'ignore') if WINDOWS else string return string.encode().decode('ascii', 'ignore') if WINDOWS else string
def colorstr(*input): def colorstr(*input):
# Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world') """Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world')."""
*args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string *args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
colors = { colors = {
'black': '\033[30m', # basic colors 'black': '\033[30m', # basic colors
@ -570,7 +570,8 @@ def colorstr(*input):
class TryExcept(contextlib.ContextDecorator): class TryExcept(contextlib.ContextDecorator):
# YOLOv8 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager """YOLOv8 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager."""
def __init__(self, msg='', verbose=True): def __init__(self, msg='', verbose=True):
self.msg = msg self.msg = msg
self.verbose = verbose self.verbose = verbose
@ -585,7 +586,8 @@ class TryExcept(contextlib.ContextDecorator):
def threaded(func): def threaded(func):
# Multi-threads a target function and returns thread. Usage: @threaded decorator """Multi-threads a target function and returns thread. Usage: @threaded decorator."""
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True) thread = threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True)
thread.start() thread.start()
@ -703,13 +705,13 @@ def deprecation_warn(arg, new_arg, version=None):
def clean_url(url): def clean_url(url):
# Strip auth from URL, i.e. https://url.com/file.txt?auth -> https://url.com/file.txt """Strip auth from URL, i.e. https://url.com/file.txt?auth -> https://url.com/file.txt."""
url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/ url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/
return urllib.parse.unquote(url).split('?')[0] # '%2F' to '/', split https://url.com/file.txt?auth return urllib.parse.unquote(url).split('?')[0] # '%2F' to '/', split https://url.com/file.txt?auth
def url2file(url): def url2file(url):
# Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt """Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt."""
return Path(clean_url(url)).name return Path(clean_url(url)).name

@ -15,20 +15,20 @@ except (ImportError, AssertionError):
COMET_MODE = os.getenv('COMET_MODE', 'online') COMET_MODE = os.getenv('COMET_MODE', 'online')
COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'YOLOv8') COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'YOLOv8')
# determines how many batches of image predictions to log from the validation set # Determines how many batches of image predictions to log from the validation set
COMET_EVAL_BATCH_LOGGING_INTERVAL = int(os.getenv('COMET_EVAL_BATCH_LOGGING_INTERVAL', 1)) COMET_EVAL_BATCH_LOGGING_INTERVAL = int(os.getenv('COMET_EVAL_BATCH_LOGGING_INTERVAL', 1))
# determines whether to log confusion matrix every evaluation epoch # Determines whether to log confusion matrix every evaluation epoch
COMET_EVAL_LOG_CONFUSION_MATRIX = (os.getenv('COMET_EVAL_LOG_CONFUSION_MATRIX', 'true').lower() == 'true') COMET_EVAL_LOG_CONFUSION_MATRIX = (os.getenv('COMET_EVAL_LOG_CONFUSION_MATRIX', 'true').lower() == 'true')
# determines whether to log image predictions every evaluation epoch # Determines whether to log image predictions every evaluation epoch
COMET_EVAL_LOG_IMAGE_PREDICTIONS = (os.getenv('COMET_EVAL_LOG_IMAGE_PREDICTIONS', 'true').lower() == 'true') COMET_EVAL_LOG_IMAGE_PREDICTIONS = (os.getenv('COMET_EVAL_LOG_IMAGE_PREDICTIONS', 'true').lower() == 'true')
COMET_MAX_IMAGE_PREDICTIONS = int(os.getenv('COMET_MAX_IMAGE_PREDICTIONS', 100)) COMET_MAX_IMAGE_PREDICTIONS = int(os.getenv('COMET_MAX_IMAGE_PREDICTIONS', 100))
# ensures certain logging functions only run for supported tasks # Ensures certain logging functions only run for supported tasks
COMET_SUPPORTED_TASKS = ['detect'] COMET_SUPPORTED_TASKS = ['detect']
# scales reported confidence scores (0.0-1.0) by this value # Scales reported confidence scores (0.0-1.0) by this value
COMET_MAX_CONFIDENCE_SCORE = int(os.getenv('COMET_MAX_CONFIDENCE_SCORE', 100)) COMET_MAX_CONFIDENCE_SCORE = int(os.getenv('COMET_MAX_CONFIDENCE_SCORE', 100))
# names of plots created by YOLOv8 that are logged to Comet # Names of plots created by YOLOv8 that are logged to Comet
EVALUATION_PLOT_NAMES = 'F1_curve', 'P_curve', 'R_curve', 'PR_curve', 'confusion_matrix' EVALUATION_PLOT_NAMES = 'F1_curve', 'P_curve', 'R_curve', 'PR_curve', 'confusion_matrix'
LABEL_PLOT_NAMES = 'labels', 'labels_correlogram' LABEL_PLOT_NAMES = 'labels', 'labels_correlogram'
@ -43,7 +43,7 @@ def _get_experiment_type(mode, project_name):
def _create_experiment(args): def _create_experiment(args):
# Ensures that the experiment object is only created in a single process during distributed training. """Ensures that the experiment object is only created in a single process during distributed training."""
if RANK not in (-1, 0): if RANK not in (-1, 0):
return return
try: try:
@ -83,13 +83,13 @@ def _scale_bounding_box_to_original_image_shape(box, resized_image_shape, origin
resized_image_height, resized_image_width = resized_image_shape resized_image_height, resized_image_width = resized_image_shape
# convert normalized xywh format predictions to xyxy in resized scale format # Convert normalized xywh format predictions to xyxy in resized scale format
box = ops.xywhn2xyxy(box, h=resized_image_height, w=resized_image_width) box = ops.xywhn2xyxy(box, h=resized_image_height, w=resized_image_width)
# scale box predictions from resized image scale back to original image scale # Scale box predictions from resized image scale back to original image scale
box = ops.scale_boxes(resized_image_shape, box, original_image_shape, ratio_pad) box = ops.scale_boxes(resized_image_shape, box, original_image_shape, ratio_pad)
# Convert bounding box format from xyxy to xywh for Comet logging # Convert bounding box format from xyxy to xywh for Comet logging
box = ops.xyxy2xywh(box) box = ops.xyxy2xywh(box)
# adjust xy center to correspond top-left corner # Adjust xy center to correspond top-left corner
box[:2] -= box[2:] / 2 box[:2] -= box[2:] / 2
box = box.tolist() box = box.tolist()

@ -244,7 +244,7 @@ def check_requirements(requirements=ROOT.parent / 'requirements.txt', exclude=()
def check_suffix(file='yolov8n.pt', suffix='.pt', msg=''): def check_suffix(file='yolov8n.pt', suffix='.pt', msg=''):
# Check file(s) for acceptable suffix """Check file(s) for acceptable suffix."""
if file and suffix: if file and suffix:
if isinstance(suffix, str): if isinstance(suffix, str):
suffix = (suffix, ) suffix = (suffix, )
@ -255,7 +255,7 @@ def check_suffix(file='yolov8n.pt', suffix='.pt', msg=''):
def check_yolov5u_filename(file: str, verbose: bool = True): def check_yolov5u_filename(file: str, verbose: bool = True):
# Replace legacy YOLOv5 filenames with updated YOLOv5u filenames """Replace legacy YOLOv5 filenames with updated YOLOv5u filenames."""
if ('yolov3' in file or 'yolov5' in file) and 'u' not in file: if ('yolov3' in file or 'yolov5' in file) and 'u' not in file:
original_file = file original_file = file
file = re.sub(r'(.*yolov5([nsmlx]))\.pt', '\\1u.pt', file) # i.e. yolov5n.pt -> yolov5nu.pt file = re.sub(r'(.*yolov5([nsmlx]))\.pt', '\\1u.pt', file) # i.e. yolov5n.pt -> yolov5nu.pt
@ -269,7 +269,7 @@ def check_yolov5u_filename(file: str, verbose: bool = True):
def check_file(file, suffix='', download=True, hard=True): def check_file(file, suffix='', download=True, hard=True):
# Search/download file (if necessary) and return path """Search/download file (if necessary) and return path."""
check_suffix(file, suffix) # optional check_suffix(file, suffix) # optional
file = str(file).strip() # convert to string and strip spaces file = str(file).strip() # convert to string and strip spaces
file = check_yolov5u_filename(file) # yolov5n -> yolov5nu file = check_yolov5u_filename(file) # yolov5n -> yolov5nu
@ -300,7 +300,7 @@ def check_yaml(file, suffix=('.yaml', '.yml'), hard=True):
def check_imshow(warn=False): def check_imshow(warn=False):
# Check if environment supports image displays """Check if environment supports image displays."""
try: try:
assert not any((is_colab(), is_kaggle(), is_docker())) assert not any((is_colab(), is_kaggle(), is_docker()))
cv2.imshow('test', np.zeros((1, 1, 3))) cv2.imshow('test', np.zeros((1, 1, 3)))
@ -346,9 +346,10 @@ def git_describe(path=ROOT): # path must be a directory
def print_args(args: Optional[dict] = None, show_file=True, show_func=False): def print_args(args: Optional[dict] = None, show_file=True, show_func=False):
# Print function arguments (optional args dict) """Print function arguments (optional args dict)."""
def strip_auth(v): def strip_auth(v):
# Clean longer Ultralytics HUB URLs by stripping potential authentication information """Clean longer Ultralytics HUB URLs by stripping potential authentication information."""
return clean_url(v) if (isinstance(v, str) and v.startswith('http') and len(v) > 100) else v return clean_url(v) if (isinstance(v, str) and v.startswith('http') and len(v) > 100) else v
x = inspect.currentframe().f_back # previous frame x = inspect.currentframe().f_back # previous frame

@ -59,6 +59,6 @@ def generate_ddp_command(world_size, trainer):
def ddp_cleanup(trainer, file): def ddp_cleanup(trainer, file):
# delete temp file if created """Delete temp file if created."""
if f'{id(trainer)}.py' in file: # if temp_file suffix in file if f'{id(trainer)}.py' in file: # if temp_file suffix in file
os.remove(file) os.remove(file)

@ -21,7 +21,7 @@ GITHUB_ASSET_STEMS = [Path(k).stem for k in GITHUB_ASSET_NAMES]
def is_url(url, check=True): def is_url(url, check=True):
# Check if string is URL and check if URL exists """Check if string is URL and check if URL exists."""
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
url = str(url) url = str(url)
result = parse.urlparse(url) result = parse.urlparse(url)
@ -141,11 +141,11 @@ def safe_download(url,
def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'): def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'):
# Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v6.2', etc. """Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v6.2', etc."""
from ultralytics.yolo.utils import SETTINGS # scoped for circular import from ultralytics.yolo.utils import SETTINGS # scoped for circular import
def github_assets(repository, version='latest'): def github_assets(repository, version='latest'):
# Return GitHub repo tag and assets (i.e. ['yolov8n.pt', 'yolov8s.pt', ...]) """Return GitHub repo tag and assets (i.e. ['yolov8n.pt', 'yolov8s.pt', ...])."""
if version != 'latest': if version != 'latest':
version = f'tags/{version}' # i.e. tags/v6.2 version = f'tags/{version}' # i.e. tags/v6.2
response = requests.get(f'https://api.github.com/repos/{repository}/releases/{version}').json() # github api response = requests.get(f'https://api.github.com/repos/{repository}/releases/{version}').json() # github api

@ -8,7 +8,8 @@ from pathlib import Path
class WorkingDirectory(contextlib.ContextDecorator): class WorkingDirectory(contextlib.ContextDecorator):
# Usage: @WorkingDirectory(dir) decorator or 'with WorkingDirectory(dir):' context manager """Usage: @WorkingDirectory(dir) decorator or 'with WorkingDirectory(dir):' context manager."""
def __init__(self, new_dir): def __init__(self, new_dir):
self.dir = new_dir # new dir self.dir = new_dir # new dir
self.cwd = Path.cwd().resolve() # current dir self.cwd = Path.cwd().resolve() # current dir
@ -56,19 +57,19 @@ def increment_path(path, exist_ok=False, sep='', mkdir=False):
def file_age(path=__file__): def file_age(path=__file__):
# Return days since last file update """Return days since last file update."""
dt = (datetime.now() - datetime.fromtimestamp(Path(path).stat().st_mtime)) # delta dt = (datetime.now() - datetime.fromtimestamp(Path(path).stat().st_mtime)) # delta
return dt.days # + dt.seconds / 86400 # fractional days return dt.days # + dt.seconds / 86400 # fractional days
def file_date(path=__file__): def file_date(path=__file__):
# Return human-readable file modification date, i.e. '2021-3-26' """Return human-readable file modification date, i.e. '2021-3-26'."""
t = datetime.fromtimestamp(Path(path).stat().st_mtime) t = datetime.fromtimestamp(Path(path).stat().st_mtime)
return f'{t.year}-{t.month}-{t.day}' return f'{t.year}-{t.month}-{t.day}'
def file_size(path): def file_size(path):
# Return file/dir size (MB) """Return file/dir size (MB)."""
if isinstance(path, (str, Path)): if isinstance(path, (str, Path)):
mb = 1 << 20 # bytes to MiB (1024 ** 2) mb = 1 << 20 # bytes to MiB (1024 ** 2)
path = Path(path) path = Path(path)
@ -80,6 +81,6 @@ def file_size(path):
def get_latest_run(search_dir='.'): def get_latest_run(search_dir='.'):
# Return path to most recent 'last.pt' in /runs (i.e. to --resume from) """Return path to most recent 'last.pt' in /runs (i.e. to --resume from)."""
last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True) last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
return max(last_list, key=os.path.getctime) if last_list else '' return max(last_list, key=os.path.getctime) if last_list else ''

@ -11,7 +11,8 @@ from .ops import ltwh2xywh, ltwh2xyxy, resample_segments, xywh2ltwh, xywh2xyxy,
def _ntuple(n): def _ntuple(n):
# From PyTorch internals """From PyTorch internals."""
def parse(x): def parse(x):
return x if isinstance(x, abc.Iterable) else tuple(repeat(x, n)) return x if isinstance(x, abc.Iterable) else tuple(repeat(x, n))
@ -29,7 +30,7 @@ __all__ = 'Bboxes', # tuple or list
class Bboxes: class Bboxes:
"""Now only numpy is supported""" """Now only numpy is supported."""
def __init__(self, bboxes, format='xyxy') -> None: def __init__(self, bboxes, format='xyxy') -> None:
assert format in _formats assert format in _formats
@ -80,7 +81,7 @@ class Bboxes:
return (self.bboxes[:, 2] - self.bboxes[:, 0]) * (self.bboxes[:, 3] - self.bboxes[:, 1]) return (self.bboxes[:, 2] - self.bboxes[:, 0]) * (self.bboxes[:, 3] - self.bboxes[:, 1])
# def denormalize(self, w, h): # def denormalize(self, w, h):
# if not self.normalized: # if not self.normalized:
# return # return
# assert (self.bboxes <= 1.0).all() # assert (self.bboxes <= 1.0).all()
# self.bboxes[:, 0::2] *= w # self.bboxes[:, 0::2] *= w
@ -207,7 +208,7 @@ class Instances:
self._bboxes.areas() self._bboxes.areas()
def scale(self, scale_w, scale_h, bbox_only=False): def scale(self, scale_w, scale_h, bbox_only=False):
"""this might be similar with denormalize func but without normalized sign""" """this might be similar with denormalize func but without normalized sign."""
self._bboxes.mul(scale=(scale_w, scale_h, scale_w, scale_h)) self._bboxes.mul(scale=(scale_w, scale_h, scale_w, scale_h))
if bbox_only: if bbox_only:
return return
@ -240,7 +241,7 @@ class Instances:
self.normalized = True self.normalized = True
def add_padding(self, padw, padh): def add_padding(self, padw, padh):
# handle rect and mosaic situation """Handle rect and mosaic situation."""
assert not self.normalized, 'you should add padding with absolute coordinates.' assert not self.normalized, 'you should add padding with absolute coordinates.'
self._bboxes.add(offset=(padw, padh, padw, padh)) self._bboxes.add(offset=(padw, padh, padw, padh))
self.segments[..., 0] += padw self.segments[..., 0] += padw

@ -9,7 +9,8 @@ from .tal import bbox2dist
class VarifocalLoss(nn.Module): class VarifocalLoss(nn.Module):
# Varifocal loss by Zhang et al. https://arxiv.org/abs/2008.13367 """Varifocal loss by Zhang et al. https://arxiv.org/abs/2008.13367."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -29,7 +30,7 @@ class BboxLoss(nn.Module):
self.use_dfl = use_dfl self.use_dfl = use_dfl
def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask): def forward(self, pred_dist, pred_bboxes, anchor_points, target_bboxes, target_scores, target_scores_sum, fg_mask):
# IoU loss """IoU loss."""
weight = torch.masked_select(target_scores.sum(-1), fg_mask).unsqueeze(-1) weight = torch.masked_select(target_scores.sum(-1), fg_mask).unsqueeze(-1)
iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True) iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True)
loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum
@ -46,7 +47,7 @@ class BboxLoss(nn.Module):
@staticmethod @staticmethod
def _df_loss(pred_dist, target): def _df_loss(pred_dist, target):
# Return sum of left and right DFL losses """Return sum of left and right DFL losses."""
# Distribution Focal Loss (DFL) proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391 # Distribution Focal Loss (DFL) proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391
tl = target.long() # target left tl = target.long() # target left
tr = tl + 1 # target right tr = tl + 1 # target right

@ -16,9 +16,9 @@ from ultralytics.yolo.utils import LOGGER, SimpleClass, TryExcept, plt_settings
OKS_SIGMA = np.array([.26, .25, .25, .35, .35, .79, .79, .72, .72, .62, .62, 1.07, 1.07, .87, .87, .89, .89]) / 10.0 OKS_SIGMA = np.array([.26, .25, .25, .35, .35, .79, .79, .72, .72, .62, .62, 1.07, 1.07, .87, .87, .89, .89]) / 10.0
# boxes # Boxes
def box_area(box): def box_area(box):
# box = xyxy(4,n) """Return box area, where box shape is xyxy(4,n)."""
return (box[2] - box[0]) * (box[3] - box[1]) return (box[2] - box[0]) * (box[3] - box[1])
@ -175,9 +175,10 @@ def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#iss
return 1.0 - 0.5 * eps, 0.5 * eps return 1.0 - 0.5 * eps, 0.5 * eps
# losses # Losses
class FocalLoss(nn.Module): class FocalLoss(nn.Module):
# Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) """Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)."""
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
super().__init__() super().__init__()
self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss() self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
@ -341,7 +342,7 @@ class ConfusionMatrix:
def smooth(y, f=0.05): def smooth(y, f=0.05):
# Box filter of fraction f """Box filter of fraction f."""
nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd) nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd)
p = np.ones(nf // 2) # ones padding p = np.ones(nf // 2) # ones padding
yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded
@ -350,7 +351,7 @@ def smooth(y, f=0.05):
@plt_settings() @plt_settings()
def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()): def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()):
# Precision-recall curve """Plots a precision-recall curve."""
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
py = np.stack(py, axis=1) py = np.stack(py, axis=1)
@ -373,7 +374,7 @@ def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()):
@plt_settings() @plt_settings()
def plot_mc_curve(px, py, save_dir=Path('mc_curve.png'), names=(), xlabel='Confidence', ylabel='Metric'): def plot_mc_curve(px, py, save_dir=Path('mc_curve.png'), names=(), xlabel='Confidence', ylabel='Metric'):
# Metric-confidence curve """Plots a metric-confidence curve."""
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
if 0 < len(names) < 21: # display per-class legend if < 21 classes if 0 < len(names) < 21: # display per-class legend if < 21 classes
@ -614,23 +615,23 @@ class Metric(SimpleClass):
return self.all_ap.mean() if len(self.all_ap) else 0.0 return self.all_ap.mean() if len(self.all_ap) else 0.0
def mean_results(self): def mean_results(self):
"""Mean of results, return mp, mr, map50, map""" """Mean of results, return mp, mr, map50, map."""
return [self.mp, self.mr, self.map50, self.map] return [self.mp, self.mr, self.map50, self.map]
def class_result(self, i): def class_result(self, i):
"""class-aware result, return p[i], r[i], ap50[i], ap[i]""" """class-aware result, return p[i], r[i], ap50[i], ap[i]."""
return self.p[i], self.r[i], self.ap50[i], self.ap[i] return self.p[i], self.r[i], self.ap50[i], self.ap[i]
@property @property
def maps(self): def maps(self):
"""mAP of each class""" """mAP of each class."""
maps = np.zeros(self.nc) + self.map maps = np.zeros(self.nc) + self.map
for i, c in enumerate(self.ap_class_index): for i, c in enumerate(self.ap_class_index):
maps[c] = self.ap[i] maps[c] = self.ap[i]
return maps return maps
def fitness(self): def fitness(self):
# Model fitness as a weighted combination of metrics """Model fitness as a weighted combination of metrics."""
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95] w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95]
return (np.array(self.mean_results()) * w).sum() return (np.array(self.mean_results()) * w).sum()
@ -800,7 +801,7 @@ class SegmentMetrics(SimpleClass):
@property @property
def ap_class_index(self): def ap_class_index(self):
# boxes and masks have the same ap_class_index """Boxes and masks have the same ap_class_index."""
return self.box.ap_class_index return self.box.ap_class_index
@property @property
@ -926,7 +927,7 @@ class ClassifyMetrics(SimpleClass):
self.speed = {'preprocess': 0.0, 'inference': 0.0, 'loss': 0.0, 'postprocess': 0.0} self.speed = {'preprocess': 0.0, 'inference': 0.0, 'loss': 0.0, 'postprocess': 0.0}
def process(self, targets, pred): def process(self, targets, pred):
# target classes and predicted classes """Target classes and predicted classes."""
pred, targets = torch.cat(pred), torch.cat(targets) pred, targets = torch.cat(pred), torch.cat(targets)
correct = (targets[:, None] == pred).float() correct = (targets[:, None] == pred).float()
acc = torch.stack((correct[:, 0], correct.max(1).values), dim=1) # (top1, top5) accuracy acc = torch.stack((correct[:, 0], correct.max(1).values), dim=1) # (top1, top5) accuracy

@ -246,7 +246,7 @@ def non_max_suppression(
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
i = i[:max_det] # limit detections i = i[:max_det] # limit detections
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean) if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4) # Update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
weights = iou * scores[None] # box weights weights = iou * scores[None] # box weights
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes

@ -21,7 +21,7 @@ from .ops import clip_boxes, scale_image, xywh2xyxy, xyxy2xywh
class Colors: class Colors:
# Ultralytics color palette https://ultralytics.com/ # Ultralytics color palette https://ultralytics.com/
def __init__(self): def __init__(self):
# hex = matplotlib.colors.TABLEAU_COLORS.values() """Initialize colors as hex = matplotlib.colors.TABLEAU_COLORS.values()."""
hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB', hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB',
'2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7') '2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7')
self.palette = [self.hex2rgb(f'#{c}') for c in hexs] self.palette = [self.hex2rgb(f'#{c}') for c in hexs]
@ -63,7 +63,7 @@ class Annotator:
else: # use cv2 else: # use cv2
self.im = im self.im = im
self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width
# pose # Pose
self.skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12], [7, 13], [6, 7], [6, 8], [7, 9], self.skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12], [7, 13], [6, 7], [6, 8], [7, 9],
[8, 10], [9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]] [8, 10], [9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]
@ -115,7 +115,7 @@ class Annotator:
alpha (float): mask transparency: 0.0 fully transparent, 1.0 opaque alpha (float): mask transparency: 0.0 fully transparent, 1.0 opaque
""" """
if self.pil: if self.pil:
# convert to numpy first # Convert to numpy first
self.im = np.asarray(self.im).copy() self.im = np.asarray(self.im).copy()
if len(masks) == 0: if len(masks) == 0:
self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255 self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255
@ -136,7 +136,7 @@ class Annotator:
im_mask_np = im_mask.byte().cpu().numpy() im_mask_np = im_mask.byte().cpu().numpy()
self.im[:] = im_mask_np if retina_masks else scale_image(im_mask_np, self.im.shape) self.im[:] = im_mask_np if retina_masks else scale_image(im_mask_np, self.im.shape)
if self.pil: if self.pil:
# convert im back to PIL and update draw # Convert im back to PIL and update draw
self.fromarray(self.im) self.fromarray(self.im)
def kpts(self, kpts, shape=(640, 640), radius=5, kpt_line=True): def kpts(self, kpts, shape=(640, 640), radius=5, kpt_line=True):
@ -152,7 +152,7 @@ class Annotator:
Note: `kpt_line=True` currently only supports human pose plotting. Note: `kpt_line=True` currently only supports human pose plotting.
""" """
if self.pil: if self.pil:
# convert to numpy first # Convert to numpy first
self.im = np.asarray(self.im).copy() self.im = np.asarray(self.im).copy()
nkpt, ndim = kpts.shape nkpt, ndim = kpts.shape
is_pose = nkpt == 17 and ndim == 3 is_pose = nkpt == 17 and ndim == 3
@ -183,11 +183,11 @@ class Annotator:
continue continue
cv2.line(self.im, pos1, pos2, [int(x) for x in self.limb_color[i]], thickness=2, lineType=cv2.LINE_AA) cv2.line(self.im, pos1, pos2, [int(x) for x in self.limb_color[i]], thickness=2, lineType=cv2.LINE_AA)
if self.pil: if self.pil:
# convert im back to PIL and update draw # Convert im back to PIL and update draw
self.fromarray(self.im) self.fromarray(self.im)
def rectangle(self, xy, fill=None, outline=None, width=1): def rectangle(self, xy, fill=None, outline=None, width=1):
# Add rectangle to image (PIL-only) """Add rectangle to image (PIL-only)."""
self.draw.rectangle(xy, fill, outline, width) self.draw.rectangle(xy, fill, outline, width)
def text(self, xy, text, txt_color=(255, 255, 255), anchor='top'): def text(self, xy, text, txt_color=(255, 255, 255), anchor='top'):
@ -202,12 +202,12 @@ class Annotator:
cv2.putText(self.im, text, xy, 0, self.lw / 3, txt_color, thickness=tf, lineType=cv2.LINE_AA) cv2.putText(self.im, text, xy, 0, self.lw / 3, txt_color, thickness=tf, lineType=cv2.LINE_AA)
def fromarray(self, im): def fromarray(self, im):
# Update self.im from a numpy array """Update self.im from a numpy array."""
self.im = im if isinstance(im, Image.Image) else Image.fromarray(im) self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
self.draw = ImageDraw.Draw(self.im) self.draw = ImageDraw.Draw(self.im)
def result(self): def result(self):
# Return annotated image as array """Return annotated image as array."""
return np.asarray(self.im) return np.asarray(self.im)
@ -217,18 +217,18 @@ def plot_labels(boxes, cls, names=(), save_dir=Path('')):
import pandas as pd import pandas as pd
import seaborn as sn import seaborn as sn
# plot dataset labels # Plot dataset labels
LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ") LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ")
b = boxes.transpose() # classes, boxes b = boxes.transpose() # classes, boxes
nc = int(cls.max() + 1) # number of classes nc = int(cls.max() + 1) # number of classes
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height']) x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
# seaborn correlogram # Seaborn correlogram
sn.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9)) sn.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200) plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200)
plt.close() plt.close()
# matplotlib labels # Matplotlib labels
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel() ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
y = ax[0].hist(cls, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8) y = ax[0].hist(cls, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
with contextlib.suppress(Exception): # color histogram bars by class with contextlib.suppress(Exception): # color histogram bars by class
@ -242,7 +242,7 @@ def plot_labels(boxes, cls, names=(), save_dir=Path('')):
sn.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9) sn.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
sn.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9) sn.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
# rectangles # Rectangles
boxes[:, 0:2] = 0.5 # center boxes[:, 0:2] = 0.5 # center
boxes = xywh2xyxy(boxes) * 1000 boxes = xywh2xyxy(boxes) * 1000
img = Image.fromarray(np.ones((1000, 1000, 3), dtype=np.uint8) * 255) img = Image.fromarray(np.ones((1000, 1000, 3), dtype=np.uint8) * 255)
@ -401,7 +401,7 @@ def plot_images(images,
@plt_settings() @plt_settings()
def plot_results(file='path/to/results.csv', dir='', segment=False, pose=False): def plot_results(file='path/to/results.csv', dir='', segment=False, pose=False):
# Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') """Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv')."""
import pandas as pd import pandas as pd
save_dir = Path(file).parent if file else Path(dir) save_dir = Path(file).parent if file else Path(dir)
if segment: if segment:
@ -436,7 +436,7 @@ def plot_results(file='path/to/results.csv', dir='', segment=False, pose=False):
def output_to_target(output, max_det=300): def output_to_target(output, max_det=300):
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf] for plotting """Convert model output to target format [batch_id, class_id, x, y, w, h, conf] for plotting."""
targets = [] targets = []
for i, o in enumerate(output): for i, o in enumerate(output):
box, conf, cls = o[:max_det, :6].cpu().split((4, 1, 1), 1) box, conf, cls = o[:max_det, :6].cpu().split((4, 1, 1), 1)

@ -48,7 +48,7 @@ def select_highest_overlaps(mask_pos, overlaps, n_max_boxes):
is_max_overlaps = is_max_overlaps.permute(0, 2, 1).to(overlaps.dtype) # (b, n_max_boxes, h*w) is_max_overlaps = is_max_overlaps.permute(0, 2, 1).to(overlaps.dtype) # (b, n_max_boxes, h*w)
mask_pos = torch.where(mask_multi_gts, is_max_overlaps, mask_pos) # (b, n_max_boxes, h*w) mask_pos = torch.where(mask_multi_gts, is_max_overlaps, mask_pos) # (b, n_max_boxes, h*w)
fg_mask = mask_pos.sum(-2) fg_mask = mask_pos.sum(-2)
# find each grid serve which gt(index) # Find each grid serve which gt(index)
target_gt_idx = mask_pos.argmax(-2) # (b, h*w) target_gt_idx = mask_pos.argmax(-2) # (b, h*w)
return target_gt_idx, fg_mask, mask_pos return target_gt_idx, fg_mask, mask_pos
@ -112,10 +112,10 @@ class TaskAlignedAssigner(nn.Module):
target_gt_idx, fg_mask, mask_pos = select_highest_overlaps(mask_pos, overlaps, self.n_max_boxes) target_gt_idx, fg_mask, mask_pos = select_highest_overlaps(mask_pos, overlaps, self.n_max_boxes)
# assigned target # Assigned target
target_labels, target_bboxes, target_scores = self.get_targets(gt_labels, gt_bboxes, target_gt_idx, fg_mask) target_labels, target_bboxes, target_scores = self.get_targets(gt_labels, gt_bboxes, target_gt_idx, fg_mask)
# normalize # Normalize
align_metric *= mask_pos align_metric *= mask_pos
pos_align_metrics = align_metric.amax(axis=-1, keepdim=True) # b, max_num_obj pos_align_metrics = align_metric.amax(axis=-1, keepdim=True) # b, max_num_obj
pos_overlaps = (overlaps * mask_pos).amax(axis=-1, keepdim=True) # b, max_num_obj pos_overlaps = (overlaps * mask_pos).amax(axis=-1, keepdim=True) # b, max_num_obj
@ -125,13 +125,13 @@ class TaskAlignedAssigner(nn.Module):
return target_labels, target_bboxes, target_scores, fg_mask.bool(), target_gt_idx return target_labels, target_bboxes, target_scores, fg_mask.bool(), target_gt_idx
def get_pos_mask(self, pd_scores, pd_bboxes, gt_labels, gt_bboxes, anc_points, mask_gt): def get_pos_mask(self, pd_scores, pd_bboxes, gt_labels, gt_bboxes, anc_points, mask_gt):
# get in_gts mask, (b, max_num_obj, h*w) """Get in_gts mask, (b, max_num_obj, h*w)."""
mask_in_gts = select_candidates_in_gts(anc_points, gt_bboxes) mask_in_gts = select_candidates_in_gts(anc_points, gt_bboxes)
# get anchor_align metric, (b, max_num_obj, h*w) # Get anchor_align metric, (b, max_num_obj, h*w)
align_metric, overlaps = self.get_box_metrics(pd_scores, pd_bboxes, gt_labels, gt_bboxes, mask_in_gts * mask_gt) align_metric, overlaps = self.get_box_metrics(pd_scores, pd_bboxes, gt_labels, gt_bboxes, mask_in_gts * mask_gt)
# get topk_metric mask, (b, max_num_obj, h*w) # Get topk_metric mask, (b, max_num_obj, h*w)
mask_topk = self.select_topk_candidates(align_metric, topk_mask=mask_gt.repeat([1, 1, self.topk]).bool()) mask_topk = self.select_topk_candidates(align_metric, topk_mask=mask_gt.repeat([1, 1, self.topk]).bool())
# merge all mask to a final mask, (b, max_num_obj, h*w) # Merge all mask to a final mask, (b, max_num_obj, h*w)
mask_pos = mask_topk * mask_in_gts * mask_gt mask_pos = mask_topk * mask_in_gts * mask_gt
return mask_pos, align_metric, overlaps return mask_pos, align_metric, overlaps
@ -145,7 +145,7 @@ class TaskAlignedAssigner(nn.Module):
ind = torch.zeros([2, self.bs, self.n_max_boxes], dtype=torch.long) # 2, b, max_num_obj ind = torch.zeros([2, self.bs, self.n_max_boxes], dtype=torch.long) # 2, b, max_num_obj
ind[0] = torch.arange(end=self.bs).view(-1, 1).repeat(1, self.n_max_boxes) # b, max_num_obj ind[0] = torch.arange(end=self.bs).view(-1, 1).repeat(1, self.n_max_boxes) # b, max_num_obj
ind[1] = gt_labels.long().squeeze(-1) # b, max_num_obj ind[1] = gt_labels.long().squeeze(-1) # b, max_num_obj
# get the scores of each grid for each gt cls # Get the scores of each grid for each gt cls
bbox_scores[mask_gt] = pd_scores[ind[0], :, ind[1]][mask_gt] # b, max_num_obj, h*w bbox_scores[mask_gt] = pd_scores[ind[0], :, ind[1]][mask_gt] # b, max_num_obj, h*w
# (b, max_num_obj, 1, 4), (b, 1, h*w, 4) # (b, max_num_obj, 1, 4), (b, 1, h*w, 4)

@ -30,7 +30,7 @@ TORCH_2_X = check_version(torch.__version__, minimum='2.0')
@contextmanager @contextmanager
def torch_distributed_zero_first(local_rank: int): def torch_distributed_zero_first(local_rank: int):
# Decorator to make all processes in distributed training wait for each local_master to do something """Decorator to make all processes in distributed training wait for each local_master to do something."""
initialized = torch.distributed.is_available() and torch.distributed.is_initialized() initialized = torch.distributed.is_available() and torch.distributed.is_initialized()
if initialized and local_rank not in (-1, 0): if initialized and local_rank not in (-1, 0):
dist.barrier(device_ids=[local_rank]) dist.barrier(device_ids=[local_rank])
@ -40,7 +40,8 @@ def torch_distributed_zero_first(local_rank: int):
def smart_inference_mode(): def smart_inference_mode():
# Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator """Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator."""
def decorate(fn): def decorate(fn):
return (torch.inference_mode if TORCH_1_9 else torch.no_grad)()(fn) return (torch.inference_mode if TORCH_1_9 else torch.no_grad)()(fn)
@ -48,7 +49,7 @@ def smart_inference_mode():
def select_device(device='', batch=0, newline=False, verbose=True): def select_device(device='', batch=0, newline=False, verbose=True):
# device = None or 'cpu' or 0 or '0' or '0,1,2,3' """Selects PyTorch Device. Options are device = None or 'cpu' or 0 or '0' or '0,1,2,3'."""
s = f'Ultralytics YOLOv{__version__} 🚀 Python-{platform.python_version()} torch-{torch.__version__} ' s = f'Ultralytics YOLOv{__version__} 🚀 Python-{platform.python_version()} torch-{torch.__version__} '
device = str(device).lower() device = str(device).lower()
for remove in 'cuda:', 'none', '(', ')', '[', ']', "'", ' ': for remove in 'cuda:', 'none', '(', ')', '[', ']', "'", ' ':
@ -84,7 +85,7 @@ def select_device(device='', batch=0, newline=False, verbose=True):
s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / (1 << 20):.0f}MiB)\n" # bytes to MB s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / (1 << 20):.0f}MiB)\n" # bytes to MB
arg = 'cuda:0' arg = 'cuda:0'
elif mps and getattr(torch, 'has_mps', False) and torch.backends.mps.is_available() and TORCH_2_X: elif mps and getattr(torch, 'has_mps', False) and torch.backends.mps.is_available() and TORCH_2_X:
# prefer MPS if available # Prefer MPS if available
s += 'MPS\n' s += 'MPS\n'
arg = 'mps' arg = 'mps'
else: # revert to CPU else: # revert to CPU
@ -97,14 +98,14 @@ def select_device(device='', batch=0, newline=False, verbose=True):
def time_sync(): def time_sync():
# PyTorch-accurate time """PyTorch-accurate time."""
if torch.cuda.is_available(): if torch.cuda.is_available():
torch.cuda.synchronize() torch.cuda.synchronize()
return time.time() return time.time()
def fuse_conv_and_bn(conv, bn): def fuse_conv_and_bn(conv, bn):
# Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/ """Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/."""
fusedconv = nn.Conv2d(conv.in_channels, fusedconv = nn.Conv2d(conv.in_channels,
conv.out_channels, conv.out_channels,
kernel_size=conv.kernel_size, kernel_size=conv.kernel_size,
@ -128,7 +129,7 @@ def fuse_conv_and_bn(conv, bn):
def fuse_deconv_and_bn(deconv, bn): def fuse_deconv_and_bn(deconv, bn):
# Fuse ConvTranspose2d() and BatchNorm2d() layers """Fuse ConvTranspose2d() and BatchNorm2d() layers."""
fuseddconv = nn.ConvTranspose2d(deconv.in_channels, fuseddconv = nn.ConvTranspose2d(deconv.in_channels,
deconv.out_channels, deconv.out_channels,
kernel_size=deconv.kernel_size, kernel_size=deconv.kernel_size,
@ -139,7 +140,7 @@ def fuse_deconv_and_bn(deconv, bn):
groups=deconv.groups, groups=deconv.groups,
bias=True).requires_grad_(False).to(deconv.weight.device) bias=True).requires_grad_(False).to(deconv.weight.device)
# prepare filters # Prepare filters
w_deconv = deconv.weight.clone().view(deconv.out_channels, -1) w_deconv = deconv.weight.clone().view(deconv.out_channels, -1)
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var))) w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
fuseddconv.weight.copy_(torch.mm(w_bn, w_deconv).view(fuseddconv.weight.shape)) fuseddconv.weight.copy_(torch.mm(w_bn, w_deconv).view(fuseddconv.weight.shape))
@ -153,7 +154,7 @@ def fuse_deconv_and_bn(deconv, bn):
def model_info(model, detailed=False, verbose=True, imgsz=640): def model_info(model, detailed=False, verbose=True, imgsz=640):
# Model information. imgsz may be int or list, i.e. imgsz=640 or imgsz=[640, 320] """Model information. imgsz may be int or list, i.e. imgsz=640 or imgsz=[640, 320]."""
if not verbose: if not verbose:
return return
n_p = get_num_params(model) n_p = get_num_params(model)
@ -174,17 +175,17 @@ def model_info(model, detailed=False, verbose=True, imgsz=640):
def get_num_params(model): def get_num_params(model):
# Return the total number of parameters in a YOLO model """Return the total number of parameters in a YOLO model."""
return sum(x.numel() for x in model.parameters()) return sum(x.numel() for x in model.parameters())
def get_num_gradients(model): def get_num_gradients(model):
# Return the total number of parameters with gradients in a YOLO model """Return the total number of parameters with gradients in a YOLO model."""
return sum(x.numel() for x in model.parameters() if x.requires_grad) return sum(x.numel() for x in model.parameters() if x.requires_grad)
def get_flops(model, imgsz=640): def get_flops(model, imgsz=640):
# Return a YOLO model's FLOPs """Return a YOLO model's FLOPs."""
try: try:
model = de_parallel(model) model = de_parallel(model)
p = next(model.parameters()) p = next(model.parameters())
@ -199,7 +200,7 @@ def get_flops(model, imgsz=640):
def initialize_weights(model): def initialize_weights(model):
# Initialize model weights to random values """Initialize model weights to random values."""
for m in model.modules(): for m in model.modules():
t = type(m) t = type(m)
if t is nn.Conv2d: if t is nn.Conv2d:
@ -224,7 +225,7 @@ def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
def make_divisible(x, divisor): def make_divisible(x, divisor):
# Returns nearest x divisible by divisor """Returns nearest x divisible by divisor."""
if isinstance(divisor, torch.Tensor): if isinstance(divisor, torch.Tensor):
divisor = int(divisor.max()) # to int divisor = int(divisor.max()) # to int
return math.ceil(x / divisor) * divisor return math.ceil(x / divisor) * divisor
@ -240,7 +241,7 @@ def copy_attr(a, b, include=(), exclude=()):
def get_latest_opset(): def get_latest_opset():
# Return second-most (for maturity) recently supported ONNX opset by this version of torch """Return second-most (for maturity) recently supported ONNX opset by this version of torch."""
return max(int(k[14:]) for k in vars(torch.onnx) if 'symbolic_opset' in k) - 1 # opset return max(int(k[14:]) for k in vars(torch.onnx) if 'symbolic_opset' in k) - 1 # opset
@ -250,22 +251,22 @@ def intersect_dicts(da, db, exclude=()):
def is_parallel(model): def is_parallel(model):
# Returns True if model is of type DP or DDP """Returns True if model is of type DP or DDP."""
return isinstance(model, (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)) return isinstance(model, (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel))
def de_parallel(model): def de_parallel(model):
# De-parallelize a model: returns single-GPU model if model is of type DP or DDP """De-parallelize a model: returns single-GPU model if model is of type DP or DDP."""
return model.module if is_parallel(model) else model return model.module if is_parallel(model) else model
def one_cycle(y1=0.0, y2=1.0, steps=100): def one_cycle(y1=0.0, y2=1.0, steps=100):
# lambda function for sinusoidal ramp from y1 to y2 https://arxiv.org/pdf/1812.01187.pdf """Returns a lambda function for sinusoidal ramp from y1 to y2 https://arxiv.org/pdf/1812.01187.pdf."""
return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1 return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
def init_seeds(seed=0, deterministic=False): def init_seeds(seed=0, deterministic=False):
# Initialize random number generator (RNG) seeds https://pytorch.org/docs/stable/notes/randomness.html """Initialize random number generator (RNG) seeds https://pytorch.org/docs/stable/notes/randomness.html."""
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)
torch.manual_seed(seed) torch.manual_seed(seed)
@ -280,14 +281,14 @@ def init_seeds(seed=0, deterministic=False):
class ModelEMA: class ModelEMA:
""" Updated Exponential Moving Average (EMA) from https://github.com/rwightman/pytorch-image-models """Updated Exponential Moving Average (EMA) from https://github.com/rwightman/pytorch-image-models
Keeps a moving average of everything in the model state_dict (parameters and buffers) Keeps a moving average of everything in the model state_dict (parameters and buffers)
For EMA details see https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage For EMA details see https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
To disable EMA set the `enabled` attribute to `False`. To disable EMA set the `enabled` attribute to `False`.
""" """
def __init__(self, model, decay=0.9999, tau=2000, updates=0): def __init__(self, model, decay=0.9999, tau=2000, updates=0):
# Create EMA """Create EMA."""
self.ema = deepcopy(de_parallel(model)).eval() # FP32 EMA self.ema = deepcopy(de_parallel(model)).eval() # FP32 EMA
self.updates = updates # number of EMA updates self.updates = updates # number of EMA updates
self.decay = lambda x: decay * (1 - math.exp(-x / tau)) # decay exponential ramp (to help early epochs) self.decay = lambda x: decay * (1 - math.exp(-x / tau)) # decay exponential ramp (to help early epochs)
@ -296,7 +297,7 @@ class ModelEMA:
self.enabled = True self.enabled = True
def update(self, model): def update(self, model):
# Update EMA parameters """Update EMA parameters."""
if self.enabled: if self.enabled:
self.updates += 1 self.updates += 1
d = self.decay(self.updates) d = self.decay(self.updates)

@ -46,7 +46,7 @@ class ClassificationTrainer(BaseTrainer):
""" """
load/create/download model for any task load/create/download model for any task
""" """
# classification models require special handling # Classification models require special handling
if isinstance(self.model, torch.nn.Module): # if model is loaded beforehand. No setup needed if isinstance(self.model, torch.nn.Module): # if model is loaded beforehand. No setup needed
return return

@ -22,8 +22,8 @@ from ultralytics.yolo.utils.torch_utils import de_parallel
class DetectionTrainer(BaseTrainer): class DetectionTrainer(BaseTrainer):
def get_dataloader(self, dataset_path, batch_size, rank=0, mode='train'): def get_dataloader(self, dataset_path, batch_size, rank=0, mode='train'):
# TODO: manage splits differently """TODO: manage splits differently."""
# calculate stride - check if model is initialized # Calculate stride - check if model is initialized
gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32) gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32)
return create_dataloader(path=dataset_path, return create_dataloader(path=dataset_path,
imgsz=self.args.imgsz, imgsz=self.args.imgsz,
@ -48,7 +48,7 @@ class DetectionTrainer(BaseTrainer):
return batch return batch
def set_model_attributes(self): def set_model_attributes(self):
# nl = de_parallel(self.model).model[-1].nl # number of detection layers (to scale hyps) """nl = de_parallel(self.model).model[-1].nl # number of detection layers (to scale hyps)."""
# self.args.box *= 3 / nl # scale to layers # self.args.box *= 3 / nl # scale to layers
# self.args.cls *= self.data["nc"] / 80 * 3 / nl # scale to classes and layers # self.args.cls *= self.data["nc"] / 80 * 3 / nl # scale to classes and layers
# self.args.cls *= (self.args.imgsz / 640) ** 2 * 3 / nl # scale to image size and layers # self.args.cls *= (self.args.imgsz / 640) ** 2 * 3 / nl # scale to image size and layers

@ -67,7 +67,7 @@ class DetectionValidator(BaseValidator):
return preds return preds
def update_metrics(self, preds, batch): def update_metrics(self, preds, batch):
# Metrics """Metrics."""
for si, pred in enumerate(preds): for si, pred in enumerate(preds):
idx = batch['batch_idx'] == si idx = batch['batch_idx'] == si
cls = batch['cls'][idx] cls = batch['cls'][idx]
@ -164,8 +164,8 @@ class DetectionValidator(BaseValidator):
return torch.tensor(correct, dtype=torch.bool, device=detections.device) return torch.tensor(correct, dtype=torch.bool, device=detections.device)
def get_dataloader(self, dataset_path, batch_size): def get_dataloader(self, dataset_path, batch_size):
# TODO: manage splits differently """TODO: manage splits differently."""
# calculate stride - check if model is initialized # Calculate stride - check if model is initialized
gs = max(int(de_parallel(self.model).stride if self.model else 0), 32) gs = max(int(de_parallel(self.model).stride if self.model else 0), 32)
return create_dataloader(path=dataset_path, return create_dataloader(path=dataset_path,
imgsz=self.args.imgsz, imgsz=self.args.imgsz,

@ -47,7 +47,7 @@ class PoseValidator(DetectionValidator):
self.sigma = OKS_SIGMA if is_pose else np.ones(nkpt) / nkpt self.sigma = OKS_SIGMA if is_pose else np.ones(nkpt) / nkpt
def update_metrics(self, preds, batch): def update_metrics(self, preds, batch):
# Metrics """Metrics."""
for si, pred in enumerate(preds): for si, pred in enumerate(preds):
idx = batch['batch_idx'] == si idx = batch['batch_idx'] == si
cls = batch['cls'][idx] cls = batch['cls'][idx]

@ -10,7 +10,7 @@ from ultralytics.yolo.v8.detect.predict import DetectionPredictor
class SegmentationPredictor(DetectionPredictor): class SegmentationPredictor(DetectionPredictor):
def postprocess(self, preds, img, orig_imgs): def postprocess(self, preds, img, orig_imgs):
# TODO: filter by classes """TODO: filter by classes."""
p = ops.non_max_suppression(preds[0], p = ops.non_max_suppression(preds[0],
self.args.conf, self.args.conf,
self.args.iou, self.args.iou,

@ -140,7 +140,7 @@ class SegLoss(Loss):
return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl) return loss.sum() * batch_size, loss.detach() # loss(box, cls, dfl)
def single_mask_loss(self, gt_mask, pred, proto, xyxy, area): def single_mask_loss(self, gt_mask, pred, proto, xyxy, area):
# Mask loss for one image """Mask loss for one image."""
pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n, 32) @ (32,80,80) -> (n,80,80) pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n, 32) @ (32,80,80) -> (n,80,80)
loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction='none') loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction='none')
return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean() return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean()

@ -52,7 +52,7 @@ class SegmentationValidator(DetectionValidator):
return p, proto return p, proto
def update_metrics(self, preds, batch): def update_metrics(self, preds, batch):
# Metrics """Metrics."""
for si, (pred, proto) in enumerate(zip(preds[0], preds[1])): for si, (pred, proto) in enumerate(zip(preds[0], preds[1])):
idx = batch['batch_idx'] == si idx = batch['batch_idx'] == si
cls = batch['cls'][idx] cls = batch['cls'][idx]
@ -179,7 +179,7 @@ class SegmentationValidator(DetectionValidator):
self.plot_masks.clear() self.plot_masks.clear()
def pred_to_json(self, predn, filename, pred_masks): def pred_to_json(self, predn, filename, pred_masks):
# Save one JSON result """Save one JSON result."""
# Example result = {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} # Example result = {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
from pycocotools.mask import encode # noqa from pycocotools.mask import encode # noqa

Loading…
Cancel
Save