`ultralytics 8.0.24` mosaic, DDP, download fixes (#703)

Co-authored-by: Laughing <61612323+Laughing-q@users.noreply.github.com>
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 899abe9f82
commit aecd17d455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,10 +37,27 @@ theme:
- navigation.footer - navigation.footer
- content.tabs.link # all code tabs change simultaneously - content.tabs.link # all code tabs change simultaneously
# Version drop-down menu # Customization
# extra: copyright: Ultralytics 2023. All rights reserved.
extra:
# version: # version:
# provider: mike # provider: mike # version drop-down menu
analytics:
provider: google
property: G-2M5EHKC0BH
social:
- icon: fontawesome/brands/github
link: https://github.com/ultralytics
- icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/company/ultralytics
- icon: fontawesome/brands/twitter
link: https://twitter.com/ultralytics
- icon: fontawesome/brands/youtube
link: https://www.youtube.com/ultralytics
- icon: fontawesome/brands/docker
link: https://hub.docker.com/r/ultralytics/ultralytics/
- icon: fontawesome/brands/python
link: https://pypi.org/project/ultralytics/
extra_css: extra_css:
- stylesheets/style.css - stylesheets/style.css

@ -1,6 +1,6 @@
# Ultralytics YOLO 🚀, GPL-3.0 license # Ultralytics YOLO 🚀, GPL-3.0 license
__version__ = "8.0.23" __version__ = "8.0.24"
from ultralytics.yolo.engine.model import YOLO from ultralytics.yolo.engine.model import YOLO
from ultralytics.yolo.utils import ops from ultralytics.yolo.utils import ops

@ -100,6 +100,7 @@ def smart_request(*args, retry=3, timeout=30, thread=True, code=-1, method="post
""" """
retry_codes = (408, 500) # retry only these codes retry_codes = (408, 500) # retry only these codes
@TryExcept(verbose=verbose)
def func(*func_args, **func_kwargs): def func(*func_args, **func_kwargs):
r = None # response r = None # response
t0 = time.time() # initial time for timer t0 = time.time() # initial time for timer
@ -146,7 +147,7 @@ class Traces:
env = 'Colab' if is_colab() else 'Kaggle' if is_kaggle() else 'Jupyter' if is_jupyter() else \ env = 'Colab' if is_colab() else 'Kaggle' if is_kaggle() else 'Jupyter' if is_jupyter() else \
'Docker' if is_docker() else platform.system() 'Docker' if is_docker() else platform.system()
self.rate_limit = 3.0 # rate limit (seconds) self.rate_limit = 3.0 # rate limit (seconds)
self.t = time.time() # rate limit timer (seconds) self.t = 0.0 # rate limit timer (seconds)
self.metadata = { self.metadata = {
"sys_argv_name": Path(sys.argv[0]).name, "sys_argv_name": Path(sys.argv[0]).name,
"install": 'git' if is_git_dir() else 'pip' if is_pip_package() else 'other', "install": 'git' if is_git_dir() else 'pip' if is_pip_package() else 'other',
@ -159,7 +160,6 @@ class Traces:
not is_github_actions_ci() and \ not is_github_actions_ci() and \
(is_pip_package() or get_git_origin_url() == "https://github.com/ultralytics/ultralytics.git") (is_pip_package() or get_git_origin_url() == "https://github.com/ultralytics/ultralytics.git")
@TryExcept(verbose=False)
def __call__(self, cfg, all_keys=False, traces_sample_rate=1.0): def __call__(self, cfg, all_keys=False, traces_sample_rate=1.0):
""" """
Sync traces data if enabled in the global settings Sync traces data if enabled in the global settings

@ -208,8 +208,8 @@ def entrypoint(debug=False):
elif a in special: elif a in special:
special[a]() special[a]()
return return
elif a in DEFAULT_CFG_DICT and DEFAULT_CFG_DICT[a] is False: elif a in DEFAULT_CFG_DICT and isinstance(DEFAULT_CFG_DICT[a], bool):
overrides[a] = True # auto-True for default False args, i.e. 'yolo show' sets show=True overrides[a] = True # auto-True for default bool args, i.e. 'yolo show' sets show=True
elif a in DEFAULT_CFG_DICT: elif a in DEFAULT_CFG_DICT:
raise SyntaxError(f"'{colorstr('red', 'bold', a)}' is a valid YOLO argument but is missing an '=' sign " raise SyntaxError(f"'{colorstr('red', 'bold', a)}' is a valid YOLO argument but is missing an '=' sign "
f"to set its value, i.e. try '{a}={DEFAULT_CFG_DICT[a]}'\n{CLI_HELP_MSG}") f"to set its value, i.e. try '{a}={DEFAULT_CFG_DICT[a]}'\n{CLI_HELP_MSG}")
@ -262,7 +262,8 @@ def entrypoint(debug=False):
LOGGER.warning(f"WARNING ⚠️ 'format=' is missing. Using default 'format={overrides['format']}'.") LOGGER.warning(f"WARNING ⚠️ 'format=' is missing. Using default 'format={overrides['format']}'.")
# Run command in python # Run command in python
getattr(model, mode)(**overrides) cfg = get_cfg(overrides=overrides)
getattr(model, mode)(**vars(cfg))
# Special modes -------------------------------------------------------------------------------------------------------- # Special modes --------------------------------------------------------------------------------------------------------

@ -44,20 +44,8 @@ class Compose:
self.transforms = transforms self.transforms = transforms
def __call__(self, data): def __call__(self, data):
mosaic_p = None
mosaic_imgsz = None
for t in self.transforms: for t in self.transforms:
if isinstance(t, Mosaic):
temp = t(data)
mosaic_p = False if temp == data else True
mosaic_imgsz = t.imgsz
data = temp
else:
if isinstance(t, RandomPerspective):
t.border = [-mosaic_imgsz // 2, -mosaic_imgsz // 2] if mosaic_p else [0, 0]
data = t(data) data = t(data)
return data return data
def append(self, transform): def append(self, transform):
@ -140,7 +128,7 @@ class Mosaic(BaseMixTransform):
labels_patch = (labels if i == 0 else labels["mix_labels"][i - 1]).copy() labels_patch = (labels if i == 0 else labels["mix_labels"][i - 1]).copy()
# Load image # Load image
img = labels_patch["img"] img = labels_patch["img"]
h, w = labels_patch["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
@ -184,11 +172,12 @@ class Mosaic(BaseMixTransform):
cls.append(labels["cls"]) cls.append(labels["cls"])
instances.append(labels["instances"]) instances.append(labels["instances"])
final_labels = { final_labels = {
"im_file": mosaic_labels[0]["im_file"],
"ori_shape": mosaic_labels[0]["ori_shape"], "ori_shape": mosaic_labels[0]["ori_shape"],
"resized_shape": (self.imgsz * 2, self.imgsz * 2), "resized_shape": (self.imgsz * 2, self.imgsz * 2),
"im_file": mosaic_labels[0]["im_file"],
"cls": np.concatenate(cls, 0), "cls": np.concatenate(cls, 0),
"instances": Instances.concatenate(instances, axis=0)} "instances": Instances.concatenate(instances, axis=0),
"mosaic_border": self.border}
final_labels["instances"].clip(self.imgsz * 2, self.imgsz * 2) final_labels["instances"].clip(self.imgsz * 2, self.imgsz * 2)
return final_labels return final_labels
@ -213,7 +202,14 @@ class MixUp(BaseMixTransform):
class RandomPerspective: class RandomPerspective:
def __init__(self, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, border=(0, 0)): def __init__(self,
degrees=0.0,
translate=0.1,
scale=0.5,
shear=0.0,
perspective=0.0,
border=(0, 0),
pre_transform=None):
self.degrees = degrees self.degrees = degrees
self.translate = translate self.translate = translate
self.scale = scale self.scale = scale
@ -221,8 +217,9 @@ class RandomPerspective:
self.perspective = perspective self.perspective = perspective
# mosaic border # mosaic border
self.border = border self.border = border
self.pre_transform = pre_transform
def affine_transform(self, img): def affine_transform(self, img, border):
# Center # Center
C = np.eye(3) C = np.eye(3)
@ -255,7 +252,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 (self.border[0] != 0) or (self.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))
else: # affine else: # affine
@ -341,6 +338,10 @@ class RandomPerspective:
Args: Args:
labels(Dict): a dict of `bboxes`, `segments`, `keypoints`. labels(Dict): a dict of `bboxes`, `segments`, `keypoints`.
""" """
if self.pre_transform and "mosaic_border" not in labels:
labels = self.pre_transform(labels)
labels.pop("ratio_pad") # do not need ratio pad
img = labels["img"] img = labels["img"]
cls = labels["cls"] cls = labels["cls"]
instances = labels.pop("instances") instances = labels.pop("instances")
@ -348,10 +349,11 @@ class RandomPerspective:
instances.convert_bbox(format="xyxy") instances.convert_bbox(format="xyxy")
instances.denormalize(*img.shape[:2][::-1]) instances.denormalize(*img.shape[:2][::-1])
self.size = img.shape[1] + self.border[1] * 2, img.shape[0] + self.border[0] * 2 # w, h border = labels.pop("mosaic_border", self.border)
self.size = img.shape[1] + border[1] * 2, img.shape[0] + border[0] * 2 # w, h
# M is affine matrix # M is affine matrix
# scale for func:`box_candidates` # scale for func:`box_candidates`
img, M, scale = self.affine_transform(img) img, M, scale = self.affine_transform(img, border)
bboxes = self.apply_bboxes(instances.bboxes, M) bboxes = self.apply_bboxes(instances.bboxes, M)
@ -513,8 +515,10 @@ class CopyPaste:
# 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]
instances = labels.pop("instances") instances = labels.pop("instances")
instances.convert_bbox(format="xyxy") instances.convert_bbox(format="xyxy")
instances.denormalize(w, h)
if self.p and len(instances.segments): if self.p and len(instances.segments):
n = len(instances) n = len(instances)
_, w, _ = im.shape # height, width, channels _, w, _ = im.shape # height, width, channels
@ -605,7 +609,7 @@ class Format:
self.batch_idx = batch_idx # keep the batch indexes self.batch_idx = batch_idx # keep the batch indexes
def __call__(self, labels): def __call__(self, labels):
img = labels["img"] img = labels.pop("img")
h, w = img.shape[:2] h, w = img.shape[:2]
cls = labels.pop("cls") cls = labels.pop("cls")
instances = labels.pop("instances") instances = labels.pop("instances")
@ -654,7 +658,7 @@ class Format:
return masks, instances, cls return masks, instances, cls
def mosaic_transforms(dataset, imgsz, hyp): def v8_transforms(dataset, imgsz, hyp):
pre_transform = Compose([ pre_transform = Compose([
Mosaic(dataset, imgsz=imgsz, p=hyp.mosaic, border=[-imgsz // 2, -imgsz // 2]), Mosaic(dataset, imgsz=imgsz, p=hyp.mosaic, border=[-imgsz // 2, -imgsz // 2]),
CopyPaste(p=hyp.copy_paste), CopyPaste(p=hyp.copy_paste),
@ -664,7 +668,7 @@ def mosaic_transforms(dataset, imgsz, hyp):
scale=hyp.scale, scale=hyp.scale,
shear=hyp.shear, shear=hyp.shear,
perspective=hyp.perspective, perspective=hyp.perspective,
border=[-imgsz // 2, -imgsz // 2], pre_transform=LetterBox(new_shape=(imgsz, imgsz)),
),]) ),])
return Compose([ return Compose([
pre_transform, pre_transform,
@ -675,23 +679,6 @@ def mosaic_transforms(dataset, imgsz, hyp):
RandomFlip(direction="horizontal", p=hyp.fliplr),]) # transforms RandomFlip(direction="horizontal", p=hyp.fliplr),]) # transforms
def affine_transforms(imgsz, hyp):
return Compose([
LetterBox(new_shape=(imgsz, imgsz)),
RandomPerspective(
degrees=hyp.degrees,
translate=hyp.translate,
scale=hyp.scale,
shear=hyp.shear,
perspective=hyp.perspective,
border=[0, 0],
),
Albumentations(p=1.0),
RandomHSV(hgain=hyp.hsv_h, sgain=hyp.hsv_s, vgain=hyp.hsv_v),
RandomFlip(direction="vertical", p=hyp.flipud),
RandomFlip(direction="horizontal", p=hyp.fliplr),]) # transforms
# Classification augmentations ----------------------------------------------------------------------------------------- # Classification augmentations -----------------------------------------------------------------------------------------
def classify_transforms(size=224): def classify_transforms(size=224):
# Transforms to apply if albumentations not installed # Transforms to apply if albumentations not installed

@ -182,6 +182,7 @@ class BaseDataset(Dataset):
def get_label_info(self, index): def get_label_info(self, index):
label = self.labels[index].copy() label = self.labels[index].copy()
label.pop("shape", None) # shape is for rect, remove it
label["img"], label["ori_shape"], label["resized_shape"] = self.load_image(index) label["img"], label["ori_shape"], label["resized_shape"] = self.load_image(index)
label["ratio_pad"] = ( label["ratio_pad"] = (
label["resized_shape"][0] / label["ori_shape"][0], label["resized_shape"][0] / label["ori_shape"][0],

@ -136,8 +136,9 @@ class YOLODataset(BaseDataset):
# TODO: use hyp config to set all these augmentations # TODO: use hyp config to set all these augmentations
def build_transforms(self, hyp=None): def build_transforms(self, hyp=None):
if self.augment: if self.augment:
mosaic = self.augment and not self.rect hyp.mosaic = hyp.mosaic if self.augment and not self.rect else 0.0
transforms = mosaic_transforms(self, self.imgsz, hyp) if mosaic else affine_transforms(self.imgsz, hyp) hyp.mixup = hyp.mixup if self.augment and not self.rect else 0.0
transforms = v8_transforms(self, self.imgsz, hyp)
else: else:
transforms = Compose([LetterBox(new_shape=(self.imgsz, self.imgsz), scaleup=False)]) transforms = Compose([LetterBox(new_shape=(self.imgsz, self.imgsz), scaleup=False)])
transforms.append( transforms.append(
@ -151,15 +152,10 @@ class YOLODataset(BaseDataset):
return transforms return transforms
def close_mosaic(self, hyp): def close_mosaic(self, hyp):
self.transforms = affine_transforms(self.imgsz, hyp) hyp.mosaic = 0.0 # set mosaic ratio=0.0
self.transforms.append( hyp.copy_paste = 0.0 # keep the same behavior as previous v8 close-mosaic
Format(bbox_format="xywh", hyp.mixup = 0.0 # keep the same behavior as previous v8 close-mosaic
normalize=True, self.transforms = self.build_transforms(hyp)
return_mask=self.use_segments,
return_keypoint=self.use_keypoints,
batch_idx=True,
mask_ratio=hyp.mask_ratio,
mask_overlap=hyp.overlap_mask))
def update_labels_info(self, label): def update_labels_info(self, label):
"""custom your label format here""" """custom your label format here"""
@ -175,8 +171,6 @@ class YOLODataset(BaseDataset):
@staticmethod @staticmethod
def collate_fn(batch): def collate_fn(batch):
# TODO: returning a dict can make thing easier and cleaner when using dataset in training
# but I don't know if this will slow down a little bit.
new_batch = {} new_batch = {}
keys = batch[0].keys() keys = batch[0].keys()
values = list(zip(*[list(b.values()) for b in batch])) values = list(zip(*[list(b.values()) for b in batch]))

@ -246,7 +246,7 @@ def check_det_dataset(dataset, autodownload=True):
r = exec(s, {'yaml': data}) # return None r = exec(s, {'yaml': data}) # return None
dt = f'({round(time.time() - t, 1)}s)' dt = f'({round(time.time() - t, 1)}s)'
s = f"success ✅ {dt}, saved to {colorstr('bold', DATASETS_DIR)}" if r in (0, None) else f"failure {dt}" s = f"success ✅ {dt}, saved to {colorstr('bold', DATASETS_DIR)}" if r in (0, None) else f"failure {dt}"
LOGGER.info(f"Dataset download {s}") LOGGER.info(f"Dataset download {s}\n")
check_font('Arial.ttf' if is_ascii(data['names']) else 'Arial.Unicode.ttf') # download fonts check_font('Arial.ttf' if is_ascii(data['names']) else 'Arial.Unicode.ttf') # download fonts
return data # dictionary return data # dictionary

@ -7,7 +7,7 @@ from ultralytics.nn.tasks import (ClassificationModel, DetectionModel, Segmentat
guess_model_task) guess_model_task)
from ultralytics.yolo.cfg import get_cfg from ultralytics.yolo.cfg import get_cfg
from ultralytics.yolo.engine.exporter import Exporter from ultralytics.yolo.engine.exporter import Exporter
from ultralytics.yolo.utils import DEFAULT_CFG, LOGGER, callbacks, yaml_load from ultralytics.yolo.utils import DEFAULT_CFG, LOGGER, RANK, callbacks, yaml_load
from ultralytics.yolo.utils.checks import check_yaml from ultralytics.yolo.utils.checks import check_yaml
from ultralytics.yolo.utils.torch_utils import smart_inference_mode from ultralytics.yolo.utils.torch_utils import smart_inference_mode
@ -205,6 +205,7 @@ class YOLO:
self.model = self.trainer.model self.model = self.trainer.model
self.trainer.train() self.trainer.train()
# update model and cfg after training # update model and cfg after training
if RANK in {0, -1}:
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

@ -135,6 +135,8 @@ class BasePredictor:
def stream_inference(self, source=None, model=None): def stream_inference(self, source=None, model=None):
self.run_callbacks("on_predict_start") self.run_callbacks("on_predict_start")
if self.args.verbose:
LOGGER.info("")
# setup model # setup model
if not self.model: if not self.model:

@ -518,7 +518,7 @@ class BaseTrainer:
last = Path(check_file(resume) if isinstance(resume, (str, Path)) else get_latest_run()) last = Path(check_file(resume) if isinstance(resume, (str, Path)) else get_latest_run())
args_yaml = last.parent.parent / 'args.yaml' # train options yaml args_yaml = last.parent.parent / 'args.yaml' # train options yaml
assert args_yaml.is_file(), \ assert args_yaml.is_file(), \
FileNotFoundError('Resume checkpoint f{last} not found. ' FileNotFoundError(f'Resume checkpoint {last} not found. '
'Please pass a valid checkpoint to resume from, i.e. yolo resume=path/to/last.pt') 'Please pass a valid checkpoint to resume from, i.e. yolo resume=path/to/last.pt')
args = get_cfg(args_yaml) # replace args = get_cfg(args_yaml) # replace
args.model, resume = str(last), True # reinstate args.model, resume = str(last), True # reinstate

@ -93,8 +93,7 @@ def check_version(current: str = "0.0.0",
Returns: Returns:
bool: True if minimum version is met, False otherwise. bool: True if minimum version is met, False otherwise.
""" """
from pkg_resources import parse_version current, minimum = (pkg.parse_version(x) for x in (current, minimum))
current, minimum = (parse_version(x) for x in (current, minimum))
result = (current == minimum) if pinned else (current >= minimum) # bool result = (current == minimum) if pinned else (current >= minimum) # bool
warning_message = f"WARNING ⚠️ {name}{minimum} is required by YOLOv8, but {name}{current} is currently installed" warning_message = f"WARNING ⚠️ {name}{minimum} is required by YOLOv8, but {name}{current} is currently installed"
if hard: if hard:

@ -1,28 +1,30 @@
# Ultralytics YOLO 🚀, GPL-3.0 license # Ultralytics YOLO 🚀, GPL-3.0 license
import contextlib import contextlib
import os
import subprocess import subprocess
import urllib
from itertools import repeat from itertools import repeat
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
from pathlib import Path from pathlib import Path
from urllib import parse, request
from zipfile import ZipFile from zipfile import ZipFile
import requests import requests
import torch import torch
from tqdm import tqdm
from ultralytics.yolo.utils import LOGGER from ultralytics.yolo.utils import LOGGER
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
try: with contextlib.suppress(Exception):
url = str(url) url = str(url)
result = urllib.parse.urlparse(url) result = parse.urlparse(url)
assert all([result.scheme, result.netloc]) # check if is url assert all([result.scheme, result.netloc]) # check if is url
return (urllib.request.urlopen(url).getcode() == 200) if check else True # check if exists online if check:
except (AssertionError, urllib.request.HTTPError): with request.urlopen(url) as response:
return response.getcode() == 200 # check if exists online
return True
return False return False
@ -57,35 +59,50 @@ def safe_download(url,
else: # does not exist else: # does not exist
assert dir or file, 'dir or file required for download' assert dir or file, 'dir or file required for download'
f = dir / Path(url).name if dir else Path(file) f = dir / Path(url).name if dir else Path(file)
LOGGER.info(f'Downloading {url} to {f}...') desc = f'Downloading {url} to {f}'
LOGGER.info(f'{desc}...')
f.parent.mkdir(parents=True, exist_ok=True) # make directory if missing f.parent.mkdir(parents=True, exist_ok=True) # make directory if missing
for i in range(retry + 1): for i in range(retry + 1):
try: try:
if curl or i > 0: # curl download with retry, continue if curl or i > 0: # curl download with retry, continue
s = 'sS' * (not progress) # silent s = 'sS' * (not progress) # silent
r = os.system(f'curl -# -{s}L "{url}" -o "{f}" --retry 9 -C -') r = subprocess.run(['curl', '-#', f'-{s}L', url, '-o', f, '--retry', '9', '-C', '-']).returncode
else: # torch download assert r == 0, f'Curl return value {r}'
r = torch.hub.download_url_to_file(url, f, progress=progress) else: # urllib download
assert r in {0, None} method = 'torch'
except Exception as e: if method == 'torch':
if i >= retry: torch.hub.download_url_to_file(url, f, progress=progress)
raise ConnectionError(f'❌ Download failure for {url}') from e else:
LOGGER.warning(f'⚠️ Download failure, retrying {i + 1}/{retry} {url}...') from ultralytics.yolo.utils import TQDM_BAR_FORMAT
continue with request.urlopen(url) as response, tqdm(total=int(response.getheader("Content-Length", 0)),
desc=desc,
disable=not progress,
unit='B',
unit_scale=True,
unit_divisor=1024,
bar_format=TQDM_BAR_FORMAT) as pbar:
with open(f, "wb") as f_opened:
for data in response:
f_opened.write(data)
pbar.update(len(data))
if f.exists(): if f.exists():
if f.stat().st_size > min_bytes: if f.stat().st_size > min_bytes:
break # success break # success
f.unlink() # remove partial downloads f.unlink() # remove partial downloads
except Exception as e:
if i >= retry:
raise ConnectionError(f'❌ Download failure for {url}') from e
LOGGER.warning(f'⚠️ Download failure, retrying {i + 1}/{retry} {url}...')
if unzip and f.exists() and f.suffix in {'.zip', '.tar', '.gz'}: if unzip and f.exists() and f.suffix in {'.zip', '.tar', '.gz'}:
LOGGER.info(f'Unzipping {f}...') LOGGER.info(f'Unzipping {f}...')
if f.suffix == '.zip': if f.suffix == '.zip':
ZipFile(f).extractall(path=f.parent) # unzip ZipFile(f).extractall(path=f.parent) # unzip
elif f.suffix == '.tar': elif f.suffix == '.tar':
os.system(f'tar xf {f} --directory {f.parent}') # unzip subprocess.run(['tar', 'xf', f, '--directory', f.parent], check=True) # unzip
elif f.suffix == '.gz': elif f.suffix == '.gz':
os.system(f'tar xfz {f} --directory {f.parent}') # unzip subprocess.run(['tar', 'xfz', f, '--directory', f.parent], check=True) # unzip
if delete: if delete:
f.unlink() # remove zip f.unlink() # remove zip
@ -95,7 +112,6 @@ def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'):
from ultralytics.yolo.utils import SETTINGS from ultralytics.yolo.utils import SETTINGS
def github_assets(repository, version='latest'): def github_assets(repository, version='latest'):
# Return GitHub repo tag and assets (i.e. ['yolov8n.pt', 'yolov5m.pt', ...])
# 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
@ -109,7 +125,7 @@ def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'):
return str(SETTINGS['weights_dir'] / file) return str(SETTINGS['weights_dir'] / file)
else: else:
# URL specified # URL specified
name = Path(urllib.parse.unquote(str(file))).name # decode '%2F' to '/' etc. name = Path(parse.unquote(str(file))).name # decode '%2F' to '/' etc.
if str(file).startswith(('http:/', 'https:/')): # download if str(file).startswith(('http:/', 'https:/')): # download
url = str(file).replace(':/', '://') # Pathlib turns :// -> :/ url = str(file).replace(':/', '://') # Pathlib turns :// -> :/
file = name.split('?')[0] # parse authentication https://url.com/file.txt?auth... file = name.split('?')[0] # parse authentication https://url.com/file.txt?auth...
@ -128,7 +144,7 @@ def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'):
tag, assets = github_assets(repo) # latest release tag, assets = github_assets(repo) # latest release
except Exception: except Exception:
try: try:
tag = subprocess.check_output('git tag', shell=True, stderr=subprocess.STDOUT).decode().split()[-1] tag = subprocess.check_output(["git", "tag"]).decode().split()[-1]
except Exception: except Exception:
tag = release tag = release

@ -10,10 +10,11 @@ import numpy as np
import pandas as pd import pandas as pd
import torch import torch
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from PIL import __version__ as pil_version
from ultralytics.yolo.utils import threaded from ultralytics.yolo.utils import threaded
from .checks import check_font, is_ascii from .checks import check_font, check_version, is_ascii
from .files import increment_path from .files import increment_path
from .ops import clip_coords, scale_image, xywh2xyxy, xyxy2xywh from .ops import clip_coords, scale_image, xywh2xyxy, xyxy2xywh
@ -46,6 +47,7 @@ class Annotator:
non_ascii = not is_ascii(example) # non-latin labels, i.e. asian, arabic, cyrillic non_ascii = not is_ascii(example) # non-latin labels, i.e. asian, arabic, cyrillic
self.pil = pil or non_ascii self.pil = pil or non_ascii
if self.pil: # use PIL if self.pil: # use PIL
self.pil_9_2_0_check = check_version(pil_version, '9.2.0') # deprecation check
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)
try: try:
@ -65,8 +67,10 @@ class Annotator:
if self.pil or not is_ascii(label): if self.pil or not is_ascii(label):
self.draw.rectangle(box, width=self.lw, outline=color) # box self.draw.rectangle(box, width=self.lw, outline=color) # box
if label: if label:
w, h = self.font.getsize(label) # text width, height (WARNING: deprecated) in 9.2.0 if self.pil_9_2_0_check:
# _, _, w, h = self.font.getbbox(label) # text width, height (New) _, _, w, h = self.font.getbbox(label) # text width, height (New)
else:
w, h = self.font.getsize(label) # text width, height (Old, deprecated in 9.2.0)
outside = box[1] - h >= 0 # label fits outside box outside = box[1] - h >= 0 # label fits outside box
self.draw.rectangle( self.draw.rectangle(
(box[0], box[1] - h if outside else box[1], box[0] + w + 1, (box[0], box[1] - h if outside else box[1], box[0] + w + 1,

@ -58,7 +58,7 @@ def DDP_model(model):
def select_device(device='', batch=0, newline=False): def select_device(device='', batch=0, newline=False):
# device = None or 'cpu' or 0 or '0' or '0,1,2,3' # device = None or 'cpu' or 0 or '0' or '0,1,2,3'
from ultralytics import __version__ from ultralytics import __version__
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', '(', ')', '[', ']', "'", ' ':
device = device.replace(remove, '') # to string, 'cuda:0' -> '0' and '(0, 1)' -> '0,1' device = device.replace(remove, '') # to string, 'cuda:0' -> '0' and '(0, 1)' -> '0,1'

Loading…
Cancel
Save