From 84948651cd9966d9bf330c6e632e9c4d19b93e1c Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Mon, 3 Apr 2023 02:36:58 +0200 Subject: [PATCH] `8.0.60` new HUB training syntax (#1753) Co-authored-by: Rafael Pierre <97888102+rafaelvp-db@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ayush Chaurasia Co-authored-by: Semih Demirel <85176438+semihhdemirel@users.noreply.github.com> --- .github/workflows/ci.yaml | 28 +++++- docker/Dockerfile | 1 + docker/Dockerfile-arm64 | 1 + docker/Dockerfile-cpu | 1 + docs/modes/predict.md | 36 +++++++- docs/yolov5/train_custom_data.md | 1 - examples/YOLOv8-OpenCV-ONNX-Python/README.md | 2 +- examples/YOLOv8-OpenCV-ONNX-Python/main.py | 14 ++- mkdocs.yml | 4 +- setup.py | 2 +- tests/test_cli.py | 4 +- ultralytics/__init__.py | 5 +- ultralytics/hub/__init__.py | 60 +++++++------ ultralytics/hub/auth.py | 95 +++++++++++++++++--- ultralytics/hub/session.py | 82 ++++++++++++++--- ultralytics/hub/utils.py | 57 ++++++++---- ultralytics/yolo/cfg/__init__.py | 56 +++++++++++- ultralytics/yolo/engine/model.py | 23 ++++- ultralytics/yolo/engine/results.py | 2 + ultralytics/yolo/utils/__init__.py | 16 ++-- ultralytics/yolo/utils/callbacks/mlflow.py | 15 ++-- ultralytics/yolo/utils/checks.py | 2 +- ultralytics/yolo/utils/downloads.py | 6 +- ultralytics/yolo/utils/plotting.py | 2 +- ultralytics/yolo/v8/segment/predict.py | 2 +- 25 files changed, 400 insertions(+), 117 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d9d12b6..8af1964 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,7 +7,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main, updates] schedule: - cron: '0 0 * * *' # runs at 00:00 UTC every day @@ -43,16 +43,36 @@ jobs: python --version pip --version pip list - - name: Test HUB training + - name: Test HUB training (Python Usage 1) + shell: python + env: + APIKEY: ${{ secrets.ULTRALYTICS_HUB_APIKEY }} + run: | + import os + from pathlib import Path + from ultralytics import YOLO, hub + from ultralytics.yolo.utils import USER_CONFIG_DIR + Path(USER_CONFIG_DIR / 'settings.yaml').unlink() + key = os.environ['APIKEY'] + hub.reset_model(key) + model = YOLO('https://hub.ultralytics.com/models/' + key) + model.train() + - name: Test HUB training (Python Usage 2) shell: python env: APIKEY: ${{ secrets.ULTRALYTICS_HUB_APIKEY }} run: | import os - from ultralytics import hub + from pathlib import Path + from ultralytics import YOLO, hub + from ultralytics.yolo.utils import USER_CONFIG_DIR + Path(USER_CONFIG_DIR / 'settings.yaml').unlink() key = os.environ['APIKEY'] hub.reset_model(key) - hub.start(key) + key, model_id = key.split('_') + hub.login(key) + model = YOLO('https://hub.ultralytics.com/models/' + model_id) + model.train() Benchmarks: runs-on: ${{ matrix.os }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 0db9152..f9d0c4c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,7 @@ WORKDIR /usr/src/ultralytics # Copy contents # COPY . /usr/src/app (issues as not a .git directory) RUN git clone https://github.com/ultralytics/ultralytics /usr/src/ultralytics +ADD https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt /usr/src/ultralytics/ # Install pip packages RUN python3 -m pip install --upgrade pip wheel diff --git a/docker/Dockerfile-arm64 b/docker/Dockerfile-arm64 index 82ff4a9..dfddff5 100644 --- a/docker/Dockerfile-arm64 +++ b/docker/Dockerfile-arm64 @@ -22,6 +22,7 @@ WORKDIR /usr/src/ultralytics # Copy contents # COPY . /usr/src/app (issues as not a .git directory) RUN git clone https://github.com/ultralytics/ultralytics /usr/src/ultralytics +ADD https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt /usr/src/ultralytics/ # Install pip packages RUN python3 -m pip install --upgrade pip wheel diff --git a/docker/Dockerfile-cpu b/docker/Dockerfile-cpu index 364aaf4..bb4bd5b 100644 --- a/docker/Dockerfile-cpu +++ b/docker/Dockerfile-cpu @@ -22,6 +22,7 @@ WORKDIR /usr/src/ultralytics # Copy contents # COPY . /usr/src/app (issues as not a .git directory) RUN git clone https://github.com/ultralytics/ultralytics /usr/src/ultralytics +ADD https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt /usr/src/ultralytics/ # Install pip packages RUN python3 -m pip install --upgrade pip wheel diff --git a/docs/modes/predict.md b/docs/modes/predict.md index 0746488..03bc4fc 100644 --- a/docs/modes/predict.md +++ b/docs/modes/predict.md @@ -17,7 +17,7 @@ passing `stream=True` in the predictor's call method. probs = result.probs # Class probabilities for classification outputs ``` - === "Return a list with `Stream=True`" + === "Return a generator with `Stream=True`" ```python inputs = [img, img] # list of numpy arrays results = model(inputs, stream=True) # generator of Results objects @@ -54,6 +54,40 @@ whether each source can be used in streaming mode with `stream=True` ✅ and an | YouTube ✅ | `'https://youtu.be/Zgi9g1ksQHc'` | `str` | | | stream ✅ | `'rtsp://example.com/media.mp4'` | `str` | RTSP, RTMP, HTTP | + +## Arguments +`model.predict` accepts multiple arguments that control the predction operation. These arguments can be passed directly to `model.predict`: +!!! example + ``` + model.predict(source, save=True, imgsz=320, conf=0.5) + ``` + +All supported arguments: + +| Key | Value | Description | +|------------------|------------------------|----------------------------------------------------------| +| `source` | `'ultralytics/assets'` | source directory for images or videos | +| `conf` | `0.25` | object confidence threshold for detection | +| `iou` | `0.7` | intersection over union (IoU) threshold for NMS | +| `half` | `False` | use half precision (FP16) | +| `device` | `None` | device to run on, i.e. cuda device=0/1/2/3 or device=cpu | +| `show` | `False` | show results if possible | +| `save` | `False` | save images with results | +| `save_txt` | `False` | save results as .txt file | +| `save_conf` | `False` | save results with confidence scores | +| `save_crop` | `False` | save cropped images with results | +| `hide_labels` | `False` | hide labels | +| `hide_conf` | `False` | hide confidence scores | +| `max_det` | `300` | maximum number of detections per image | +| `vid_stride` | `False` | video frame-rate stride | +| `line_thickness` | `3` | bounding box thickness (pixels) | +| `visualize` | `False` | visualize model features | +| `augment` | `False` | apply image augmentation to prediction sources | +| `agnostic_nms` | `False` | class-agnostic NMS | +| `retina_masks` | `False` | use high-resolution segmentation masks | +| `classes` | `None` | filter results by class, i.e. class=0, or class=[0,2,3] | +| `boxes` | `True` | Show boxes in segmentation predictions | + ## Image and Video Formats YOLOv8 supports various image and video formats, as specified diff --git a/docs/yolov5/train_custom_data.md b/docs/yolov5/train_custom_data.md index 3d56293..e249861 100644 --- a/docs/yolov5/train_custom_data.md +++ b/docs/yolov5/train_custom_data.md @@ -96,7 +96,6 @@ names: 77: teddy bear 78: hair drier 79: toothbrush - ``` diff --git a/examples/YOLOv8-OpenCV-ONNX-Python/README.md b/examples/YOLOv8-OpenCV-ONNX-Python/README.md index 604dcfe..c9076fa 100644 --- a/examples/YOLOv8-OpenCV-ONNX-Python/README.md +++ b/examples/YOLOv8-OpenCV-ONNX-Python/README.md @@ -6,7 +6,7 @@ Just simply clone and run ```bash pip install -r requirements.txt -python main.py +python main.py --model yolov8n.onnx --img image.jpg ``` If you start from scratch: diff --git a/examples/YOLOv8-OpenCV-ONNX-Python/main.py b/examples/YOLOv8-OpenCV-ONNX-Python/main.py index 410c908..acae890 100644 --- a/examples/YOLOv8-OpenCV-ONNX-Python/main.py +++ b/examples/YOLOv8-OpenCV-ONNX-Python/main.py @@ -1,3 +1,5 @@ +import argparse + import cv2.dnn import numpy as np @@ -16,9 +18,9 @@ def draw_bounding_box(img, class_id, confidence, x, y, x_plus_w, y_plus_h): cv2.putText(img, label, (x - 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) -def main(): - model: cv2.dnn.Net = cv2.dnn.readNetFromONNX('yolov8n.onnx') - original_image: np.ndarray = cv2.imread(str(ROOT / 'assets/bus.jpg')) +def main(onnx_model, input_image): + model: cv2.dnn.Net = cv2.dnn.readNetFromONNX(onnx_model) + original_image: np.ndarray = cv2.imread(input_image) [height, width, _] = original_image.shape length = max((height, width)) image = np.zeros((length, length, 3), np.uint8) @@ -71,4 +73,8 @@ def main(): if __name__ == '__main__': - main() + parser = argparse.ArgumentParser() + parser.add_argument('--model', default='yolov8n.onnx', help='Input your onnx model.') + parser.add_argument('--img', default=str(ROOT / 'assets/bus.jpg'), help='Path to input image.') + args = parser.parse_args() + main(args.model, args.img) diff --git a/mkdocs.yml b/mkdocs.yml index 98f6cae..3ca07b3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,7 +46,7 @@ theme: - content.tabs.link # all code tabs change simultaneously # Customization -copyright: Ultralytics 2023. All rights reserved. +copyright: Ultralytics 2023. All rights reserved. extra: # version: # provider: mike # version drop-down menu @@ -167,7 +167,7 @@ nav: - Hyperparameter evolution: yolov5/hyp_evolution.md - Transfer learning with frozen layers: yolov5/transfer_learn_frozen.md - Architecture Summary: yolov5/architecture.md - - Roboflow for Datasets, Labeling, and Active Learning: yolov5/roboflow.md + - Roboflow Datasets: yolov5/roboflow.md - Neural Magic's DeepSparse: yolov5/neural_magic.md - Comet Logging: yolov5/comet.md - Clearml Logging: yolov5/clearml.md diff --git a/setup.py b/setup.py index e066bc4..ba0296e 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( 'Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Scientific/Engineering :: Image Recognition', 'Operating System :: POSIX :: Linux', - 'Operating System :: macOS', + 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', ], keywords='machine-learning, deep-learning, vision, ML, DL, AI, YOLO, YOLOv3, YOLOv5, YOLOv8, HUB, Ultralytics', entry_points={ diff --git a/tests/test_cli.py b/tests/test_cli.py index 1ba1094..3e5b8ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -56,11 +56,11 @@ def test_predict_detect(): def test_predict_segment(): - run(f"yolo predict model={MODEL}-seg.pt source={ROOT / 'assets'} imgsz=32 save") + run(f"yolo predict model={MODEL}-seg.pt source={ROOT / 'assets'} imgsz=32 save save_txt") def test_predict_classify(): - run(f"yolo predict model={MODEL}-cls.pt source={ROOT / 'assets'} imgsz=32 save") + run(f"yolo predict model={MODEL}-cls.pt source={ROOT / 'assets'} imgsz=32 save save_txt") # Export checks -------------------------------------------------------------------------------------------------------- diff --git a/ultralytics/__init__.py b/ultralytics/__init__.py index 04f4c41..dc7a2c9 100644 --- a/ultralytics/__init__.py +++ b/ultralytics/__init__.py @@ -1,8 +1,9 @@ # Ultralytics YOLO 🚀, GPL-3.0 license -__version__ = '8.0.59' +__version__ = '8.0.60' +from ultralytics.hub import start from ultralytics.yolo.engine.model import YOLO from ultralytics.yolo.utils.checks import check_yolo as checks -__all__ = '__version__', 'YOLO', 'checks' # allow simpler import +__all__ = '__version__', 'YOLO', 'checks', 'start' # allow simpler import diff --git a/ultralytics/hub/__init__.py b/ultralytics/hub/__init__.py index 420d486..15133ba 100644 --- a/ultralytics/hub/__init__.py +++ b/ultralytics/hub/__init__.py @@ -2,47 +2,51 @@ import requests -from ultralytics.hub.auth import Auth -from ultralytics.hub.session import HUBTrainingSession from ultralytics.hub.utils import PREFIX, split_key -from ultralytics.yolo.engine.model import YOLO -from ultralytics.yolo.utils import LOGGER, emojis +from ultralytics.yolo.utils import LOGGER -def start(key=''): - """ - Start training models with Ultralytics HUB. Usage: from ultralytics.hub import start; start('API_KEY') +def login(api_key=''): """ - auth = Auth(key) - model_id = split_key(key)[1] if auth.get_state() else request_api_key(auth) - if not model_id: - raise ConnectionError(emojis('Connecting with global API key is not currently supported. ❌')) + Log in to the Ultralytics HUB API using the provided API key. - session = HUBTrainingSession(model_id=model_id, auth=auth) - session.check_disk_space() + Args: + api_key (str, optional): May be an API key or a combination API key and model ID, i.e. key_id - model = YOLO(model=session.model_file, session=session) - model.train(**session.train_args) + Example: + from ultralytics import hub + hub.login('your_api_key') + """ + from ultralytics.hub.auth import Auth + Auth(api_key) -def request_api_key(auth, max_attempts=3): +def logout(): """ - Prompt the user to input their API key. Returns the model ID. + Logout Ultralytics HUB + + Example: + from ultralytics import hub + hub.logout() """ - import getpass - for attempts in range(max_attempts): - LOGGER.info(f'{PREFIX}Login. Attempt {attempts + 1} of {max_attempts}') - input_key = getpass.getpass( - 'Enter your Ultralytics API Key from https://hub.ultralytics.com/settings?tab=api+keys:\n') - auth.api_key, model_id = split_key(input_key) + LOGGER.warning('WARNING ⚠️ This method is not yet implemented.') + - if auth.authenticate(): - LOGGER.info(f'{PREFIX}Authenticated ✅') - return model_id +def start(key=''): + """ + Start training models with Ultralytics HUB (DEPRECATED). + + Args: + key (str, optional): A string containing either the API key and model ID combination (apikey_modelid), + or the full model URL (https://hub.ultralytics.com/models/apikey_modelid). + """ + LOGGER.warning(f""" +WARNING ⚠️ ultralytics.start() is deprecated in 8.0.60. Updated usage to train your Ultralytics HUB model is below: - LOGGER.warning(f'{PREFIX}Invalid API key ⚠️\n') +from ultralytics import YOLO - raise ConnectionError(emojis(f'{PREFIX}Failed to authenticate ❌')) +model = YOLO('https://hub.ultralytics.com/models/{key}') +model.train()""") def reset_model(key=''): diff --git a/ultralytics/hub/auth.py b/ultralytics/hub/auth.py index 8655b6f..f39e265 100644 --- a/ultralytics/hub/auth.py +++ b/ultralytics/hub/auth.py @@ -2,27 +2,74 @@ import requests -from ultralytics.hub.utils import HUB_API_ROOT, request_with_credentials -from ultralytics.yolo.utils import is_colab +from ultralytics.hub.utils import HUB_API_ROOT, PREFIX, request_with_credentials +from ultralytics.yolo.utils import LOGGER, SETTINGS, emojis, is_colab, set_settings -API_KEY_PATH = 'https://hub.ultralytics.com/settings?tab=api+keys' +API_KEY_URL = 'https://hub.ultralytics.com/settings?tab=api+keys' class Auth: id_token = api_key = model_key = False - def __init__(self, api_key=None): - self.api_key = self._clean_api_key(api_key) - self.authenticate() if self.api_key else self.auth_with_cookies() + def __init__(self, api_key=''): + """ + Initialize the Auth class with an optional API key. + + Args: + api_key (str, optional): May be an API key or a combination API key and model ID, i.e. key_id + """ + # Split the input API key in case it contains a combined key_model and keep only the API key part + api_key = api_key.split('_')[0] + + # Set API key attribute as value passed or SETTINGS API key if none passed + self.api_key = api_key or SETTINGS.get('api_key', '') + + # If an API key is provided + if self.api_key: + # If the provided API key matches the API key in the SETTINGS + if self.api_key == SETTINGS.get('api_key'): + # Log that the user is already logged in + LOGGER.info(f'{PREFIX}Authenticated ✅') + return + else: + # Attempt to authenticate with the provided API key + success = self.authenticate() + # If the API key is not provided and the environment is a Google Colab notebook + elif is_colab(): + # Attempt to authenticate using browser cookies + success = self.auth_with_cookies() + else: + # Request an API key + success = self.request_api_key() - @staticmethod - def _clean_api_key(key: str) -> str: - """Strip model from key if present""" - separator = '_' - return key.split(separator)[0] if separator in key else key + # Update SETTINGS with the new API key after successful authentication + if success: + set_settings({'api_key': self.api_key}) + # Log that the new login was successful + LOGGER.info(f'{PREFIX}New authentication successful ✅') + else: + LOGGER.info(f'{PREFIX}Retrieve API key from {API_KEY_URL}') + + def request_api_key(self, max_attempts=3): + """ + Prompt the user to input their API key. Returns the model ID. + """ + import getpass + for attempts in range(max_attempts): + LOGGER.info(f'{PREFIX}Login. Attempt {attempts + 1} of {max_attempts}') + input_key = getpass.getpass(f'Enter API key from {API_KEY_URL} ') + self.api_key = input_key.split('_')[0] # remove model id if present + if self.authenticate(): + return True + raise ConnectionError(emojis(f'{PREFIX}Failed to authenticate ❌')) def authenticate(self) -> bool: - """Attempt to authenticate with server""" + """ + Attempt to authenticate with the server using either id_token or API key. + + Returns: + bool: True if authentication is successful, False otherwise. + """ try: header = self.get_auth_header() if header: @@ -33,12 +80,16 @@ class Auth: raise ConnectionError('User has not authenticated locally.') except ConnectionError: self.id_token = self.api_key = False # reset invalid + LOGGER.warning(f'{PREFIX}Invalid API key ⚠️') return False def auth_with_cookies(self) -> bool: """ Attempt to fetch authentication via cookies and set id_token. User must be logged in to HUB and running in a supported browser. + + Returns: + bool: True if authentication is successful, False otherwise. """ if not is_colab(): return False # Currently only works with Colab @@ -54,6 +105,12 @@ class Auth: return False def get_auth_header(self): + """ + Get the authentication header for making API requests. + + Returns: + dict: The authentication header if id_token or API key is set, None otherwise. + """ if self.id_token: return {'authorization': f'Bearer {self.id_token}'} elif self.api_key: @@ -62,9 +119,19 @@ class Auth: return None def get_state(self) -> bool: - """Get the authentication state""" + """ + Get the authentication state. + + Returns: + bool: True if either id_token or API key is set, False otherwise. + """ return self.id_token or self.api_key def set_api_key(self, key: str): - """Get the authentication state""" + """ + Set the API key for authentication. + + Args: + key (str): The API key string. + """ self.api_key = key diff --git a/ultralytics/hub/session.py b/ultralytics/hub/session.py index 71cc719..0f93f50 100644 --- a/ultralytics/hub/session.py +++ b/ultralytics/hub/session.py @@ -6,17 +6,62 @@ from time import sleep import requests -from ultralytics.hub.utils import HUB_API_ROOT, check_dataset_disk_space, smart_request -from ultralytics.yolo.utils import LOGGER, PREFIX, __version__, checks, emojis, is_colab, threaded +from ultralytics.hub.utils import HUB_API_ROOT, PREFIX, check_dataset_disk_space, smart_request +from ultralytics.yolo.utils import LOGGER, __version__, checks, emojis, is_colab, threaded AGENT_NAME = f'python-{__version__}-colab' if is_colab() else f'python-{__version__}-local' class HUBTrainingSession: + """ + HUB training session for Ultralytics HUB YOLO models. Handles model initialization, heartbeats, and checkpointing. + + Args: + url (str): Model identifier used to initialize the HUB training session. + + Attributes: + agent_id (str): Identifier for the instance communicating with the server. + model_id (str): Identifier for the YOLOv5 model being trained. + model_url (str): URL for the model in Ultralytics HUB. + api_url (str): API URL for the model in Ultralytics HUB. + auth_header (Dict): Authentication header for the Ultralytics HUB API requests. + rate_limits (Dict): Rate limits for different API calls (in seconds). + timers (Dict): Timers for rate limiting. + metrics_queue (Dict): Queue for the model's metrics. + model (Dict): Model data fetched from Ultralytics HUB. + alive (bool): Indicates if the heartbeat loop is active. + """ + + def __init__(self, url): + """ + Initialize the HUBTrainingSession with the provided model identifier. + + Args: + url (str): Model identifier used to initialize the HUB training session. + It can be a URL string or a model key with specific format. + + Raises: + ValueError: If the provided model identifier is invalid. + ConnectionError: If connecting with global API key is not supported. + """ + + from ultralytics.hub.auth import Auth - def __init__(self, model_id, auth): + # Parse input + if url.startswith('https://hub.ultralytics.com/models/'): + url = url.split('https://hub.ultralytics.com/models/')[-1] + if [len(x) for x in url.split('_')] == [42, 20]: + key, model_id = url.split('_') + elif len(url) == 20: + key, model_id = '', url + else: + raise ValueError(f'Invalid HUBTrainingSession input: {url}') + + # Authorize + auth = Auth(key) self.agent_id = None # identifies which instance is communicating with server self.model_id = model_id + self.model_url = f'https://hub.ultralytics.com/models/{model_id}' self.api_url = f'{HUB_API_ROOT}/v1/models/{model_id}' self.auth_header = auth.get_auth_header() self.rate_limits = {'metrics': 3.0, 'ckpt': 900.0, 'heartbeat': 300.0} # rate limits (seconds) @@ -26,16 +71,17 @@ class HUBTrainingSession: self.alive = True self._start_heartbeat() # start heartbeats self._register_signal_handlers() + LOGGER.info(f'{PREFIX}View model at {self.model_url} 🚀') def _register_signal_handlers(self): + """Register signal handlers for SIGTERM and SIGINT signals to gracefully handle termination.""" signal.signal(signal.SIGTERM, self._handle_signal) signal.signal(signal.SIGINT, self._handle_signal) def _handle_signal(self, signum, frame): """ - Prevent heartbeats from being sent on Colab after kill. - This method does not use frame, it is included as it is - passed by signal. + Handle kill signals and prevent heartbeats from being sent on Colab after termination. + This method does not use frame, it is included as it is passed by signal. """ if self.alive is True: LOGGER.info(f'{PREFIX}Kill signal received! ❌') @@ -43,15 +89,16 @@ class HUBTrainingSession: sys.exit(signum) def _stop_heartbeat(self): - """End the heartbeat loop""" + """Terminate the heartbeat loop.""" self.alive = False def upload_metrics(self): + """Upload model metrics to Ultralytics HUB.""" payload = {'metrics': self.metrics_queue.copy(), 'type': 'metrics'} smart_request('post', self.api_url, json=payload, headers=self.auth_header, code=2) def _get_model(self): - # Returns model from database by id + """Fetch and return model data from Ultralytics HUB.""" api_url = f'{HUB_API_ROOT}/v1/models/{self.model_id}' try: @@ -59,9 +106,7 @@ class HUBTrainingSession: data = response.json().get('data', None) if data.get('status', None) == 'trained': - raise ValueError( - emojis(f'Model is already trained and uploaded to ' - f'https://hub.ultralytics.com/models/{self.model_id} 🚀')) + raise ValueError(emojis(f'Model is already trained and uploaded to {self.model_url} 🚀')) if not data.get('data', None): raise ValueError('Dataset may still be processing. Please wait a minute and try again.') # RF fix @@ -88,11 +133,21 @@ class HUBTrainingSession: raise def check_disk_space(self): - if not check_dataset_disk_space(self.model['data']): + """Check if there is enough disk space for the dataset.""" + if not check_dataset_disk_space(url=self.model['data']): raise MemoryError('Not enough disk space') def upload_model(self, epoch, weights, is_best=False, map=0.0, final=False): - # Upload a model to HUB + """ + Upload a model checkpoint to Ultralytics HUB. + + Args: + epoch (int): The current training epoch. + weights (str): Path to the model weights file. + is_best (bool): Indicates if the current model is the best one so far. + map (float): Mean average precision of the model. + final (bool): Indicates if the model is the final model after training. + """ if Path(weights).is_file(): with open(weights, 'rb') as f: file = f.read() @@ -120,6 +175,7 @@ class HUBTrainingSession: @threaded def _start_heartbeat(self): + """Begin a threaded heartbeat loop to report the agent's status to Ultralytics HUB.""" while self.alive: r = smart_request('post', f'{HUB_API_ROOT}/v1/agent/heartbeat/models/{self.model_id}', diff --git a/ultralytics/hub/utils.py b/ultralytics/hub/utils.py index 2615f10..41e3770 100644 --- a/ultralytics/hub/utils.py +++ b/ultralytics/hub/utils.py @@ -22,7 +22,16 @@ HUB_API_ROOT = os.environ.get('ULTRALYTICS_HUB_API', 'https://api.ultralytics.co def check_dataset_disk_space(url='https://ultralytics.com/assets/coco128.zip', sf=2.0): - # Check that url fits on disk with safety factor sf, i.e. require 2GB free if url size is 1GB with sf=2.0 + """ + Check if there is sufficient disk space to download and store a dataset. + + Args: + url (str, optional): The URL to the dataset file. Defaults to 'https://ultralytics.com/assets/coco128.zip'. + sf (float, optional): Safety factor, the multiplier for the required free space. Defaults to 2.0. + + Returns: + bool: True if there is sufficient disk space, False otherwise. + """ gib = 1 << 30 # bytes per GiB data = int(requests.head(url).headers['Content-Length']) / gib # dataset size (GB) total, used, free = (x / gib for x in shutil.disk_usage('/')) # bytes @@ -35,7 +44,18 @@ def check_dataset_disk_space(url='https://ultralytics.com/assets/coco128.zip', s def request_with_credentials(url: str) -> any: - """ Make an ajax request with cookies attached """ + """ + Make an AJAX request with cookies attached in a Google Colab environment. + + Args: + url (str): The URL to make the request to. + + Returns: + any: The response data from the AJAX request. + + Raises: + OSError: If the function is not run in a Google Colab environment. + """ if not is_colab(): raise OSError('request_with_credentials() must run in a Colab environment') from google.colab import output # noqa @@ -95,7 +115,6 @@ def requests_with_progress(method, url, **kwargs): Returns: requests.Response: The response from the HTTP request. - """ progress = kwargs.pop('progress', False) if not progress: @@ -126,7 +145,6 @@ def smart_request(method, url, retry=3, timeout=30, thread=True, code=-1, verbos Returns: requests.Response: The HTTP response object. If the request is executed in a separate thread, returns None. - """ retry_codes = (408, 500) # retry only these codes @@ -171,8 +189,8 @@ class Traces: def __init__(self): """ Initialize Traces for error tracking and reporting if tests are not currently running. + Sets the rate limit, timer, and metadata attributes, and determines whether Traces are enabled. """ - from ultralytics.yolo.cfg import MODES, TASKS self.rate_limit = 60.0 # rate limit (seconds) self.t = 0.0 # rate limit timer (seconds) self.metadata = { @@ -187,17 +205,22 @@ class Traces: not TESTS_RUNNING and \ ONLINE and \ (is_pip_package() or get_git_origin_url() == 'https://github.com/ultralytics/ultralytics.git') - self.usage = {'tasks': {k: 0 for k in TASKS}, 'modes': {k: 0 for k in MODES}} + self._reset_usage() 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. Args: cfg (IterableSimpleNamespace): Configuration for the task and mode. all_keys (bool): Sync all items, not just non-default values. - traces_sample_rate (float): Fraction of traces captured from 0.0 to 1.0 + traces_sample_rate (float): Fraction of traces captured from 0.0 to 1.0. """ + + # Increment usage + self.usage['modes'][cfg.mode] = self.usage['modes'].get(cfg.mode, 0) + 1 + self.usage['tasks'][cfg.task] = self.usage['tasks'].get(cfg.task, 0) + 1 + t = time.time() # current time if not self.enabled or random() > traces_sample_rate: # Traces disabled or not randomly selected, do nothing @@ -207,18 +230,20 @@ class Traces: return else: # Time is over rate limiter, send trace now - self.t = t # reset rate limit timer - - # Build trace - if cfg.task in self.usage['tasks']: - self.usage['tasks'][cfg.task] += 1 - if cfg.mode in self.usage['modes']: - self.usage['modes'][cfg.mode] += 1 - trace = {'uuid': SETTINGS['uuid'], 'usage': self.usage, 'metadata': self.metadata} + trace = {'uuid': SETTINGS['uuid'], 'usage': self.usage.copy(), 'metadata': self.metadata} # Send a request to the HUB API to sync analytics smart_request('post', f'{HUB_API_ROOT}/v1/usage/anonymous', json=trace, code=3, retry=0, verbose=False) + # Reset usage and rate limit timer + self._reset_usage() + self.t = t + + def _reset_usage(self): + """Reset the usage dictionary by initializing keys for each task and mode with a value of 0.""" + from ultralytics.yolo.cfg import MODES, TASKS + self.usage = {'tasks': {k: 0 for k in TASKS}, 'modes': {k: 0 for k in MODES}} + # Run below code on hub/utils init ------------------------------------------------------------------------------------- traces = Traces() diff --git a/ultralytics/yolo/cfg/__init__.py b/ultralytics/yolo/cfg/__init__.py index 4436b2d..f81a2ef 100644 --- a/ultralytics/yolo/cfg/__init__.py +++ b/ultralytics/yolo/cfg/__init__.py @@ -9,7 +9,8 @@ from types import SimpleNamespace from typing import Dict, List, Union from ultralytics.yolo.utils import (DEFAULT_CFG, DEFAULT_CFG_DICT, DEFAULT_CFG_PATH, LOGGER, ROOT, USER_CONFIG_DIR, - IterableSimpleNamespace, __version__, checks, colorstr, yaml_load, yaml_print) + IterableSimpleNamespace, __version__, checks, colorstr, get_settings, yaml_load, + yaml_print) # Define valid tasks and modes MODES = 'train', 'val', 'predict', 'export', 'track', 'benchmark' @@ -187,6 +188,51 @@ def merge_equals_args(args: List[str]) -> List[str]: return new_args +def handle_yolo_hub(args: List[str]) -> None: + """ + Handle Ultralytics HUB command-line interface (CLI) commands. + + This function processes Ultralytics HUB CLI commands such as login and logout. + It should be called when executing a script with arguments related to HUB authentication. + + Args: + args (List[str]): A list of command line arguments + + Example: + python my_script.py hub login your_api_key + """ + from ultralytics import hub + + if args[0] == 'login': + key = args[1] if len(args) > 1 else '' + # Log in to Ultralytics HUB using the provided API key + hub.login(key) + elif args[0] == 'logout': + # Log out from Ultralytics HUB + hub.logout() + + +def handle_yolo_settings(args: List[str]) -> None: + """ + Handle YOLO settings command-line interface (CLI) commands. + + This function processes YOLO settings CLI commands such as reset. + It should be called when executing a script with arguments related to YOLO settings management. + + Args: + args (List[str]): A list of command line arguments for YOLO settings management. + + Example: + python my_script.py yolo settings reset + """ + path = USER_CONFIG_DIR / 'settings.yaml' # get SETTINGS YAML file path + if any(args) and args[0] == 'reset': + path.unlink() # delete the settings file + get_settings() # create new settings + LOGGER.info('Settings reset successfully') # inform the user that settings have been reset + yaml_print(path) # print the current settings + + def entrypoint(debug=''): """ This function is the ultralytics package entrypoint, it's responsible for parsing the command line arguments passed @@ -211,8 +257,10 @@ def entrypoint(debug=''): 'help': lambda: LOGGER.info(CLI_HELP_MSG), 'checks': checks.check_yolo, 'version': lambda: LOGGER.info(__version__), - 'settings': lambda: yaml_print(USER_CONFIG_DIR / 'settings.yaml'), + 'settings': lambda: handle_yolo_settings(args[1:]), 'cfg': lambda: yaml_print(DEFAULT_CFG_PATH), + 'hub': lambda: handle_yolo_hub(args[1:]), + 'login': lambda: handle_yolo_hub(args), 'copy-cfg': copy_default_cfg} full_args_dict = {**DEFAULT_CFG_DICT, **{k: None for k in TASKS}, **{k: None for k in MODES}, **special} @@ -255,8 +303,8 @@ def entrypoint(debug=''): overrides['task'] = a elif a in MODES: overrides['mode'] = a - elif a in special: - special[a]() + elif a.lower() in special: + special[a.lower()]() return elif a in DEFAULT_CFG_DICT and isinstance(DEFAULT_CFG_DICT[a], bool): overrides[a] = True # auto-True for default bool args, i.e. 'yolo show' sets show=True diff --git a/ultralytics/yolo/engine/model.py b/ultralytics/yolo/engine/model.py index c3aefa0..cfe537e 100644 --- a/ultralytics/yolo/engine/model.py +++ b/ultralytics/yolo/engine/model.py @@ -68,12 +68,14 @@ class YOLO: list(ultralytics.yolo.engine.results.Results): The prediction results. """ - def __init__(self, model: Union[str, Path] = 'yolov8n.pt', task=None, session=None) -> None: + def __init__(self, model: Union[str, Path] = 'yolov8n.pt', task=None) -> None: """ Initializes the YOLO model. Args: - model (str, Path): model to load or create + 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. + """ self._reset_callbacks() self.predictor = None # reuse predictor @@ -85,10 +87,16 @@ class YOLO: self.ckpt_path = None self.overrides = {} # overrides for trainer object self.metrics = None # validation/training metrics - self.session = session # HUB session + self.session = None # HUB session + model = str(model).strip() # strip spaces + + # Check if Ultralytics HUB model from https://hub.ultralytics.com + if model.startswith('https://hub.ultralytics.com/models/'): + from ultralytics.hub import HUBTrainingSession + self.session = HUBTrainingSession(model) + model = self.session.model_file # Load or create new YOLO model - model = str(model).strip() # strip spaces suffix = Path(model).suffix if not suffix and Path(model).stem in GITHUB_ASSET_STEMS: model, suffix = Path(model).with_suffix('.pt'), '.pt' # add suffix, i.e. yolov8n -> yolov8n.pt @@ -280,6 +288,7 @@ class YOLO: from ultralytics.yolo.utils.benchmarks import benchmark overrides = self.model.args.copy() overrides.update(kwargs) + overrides['mode'] = 'benchmark' overrides = {**DEFAULT_CFG_DICT, **overrides} # fill in missing overrides keys with defaults return benchmark(model=self, imgsz=overrides['imgsz'], half=overrides['half'], device=overrides['device']) @@ -293,6 +302,7 @@ class YOLO: self._check_is_pytorch_model() overrides = self.overrides.copy() overrides.update(kwargs) + overrides['mode'] = 'export' args = get_cfg(cfg=DEFAULT_CFG, overrides=overrides) args.task = self.task if args.imgsz == DEFAULT_CFG.imgsz: @@ -309,6 +319,11 @@ class YOLO: **kwargs (Any): Any number of arguments representing the training configuration. """ self._check_is_pytorch_model() + if self.session: # Ultralytics HUB session + if any(kwargs): + LOGGER.warning('WARNING ⚠️ using HUB training arguments, ignoring local training arguments.') + kwargs = self.session.train_args + self.session.check_disk_space() check_pip_update_available() overrides = self.overrides.copy() overrides.update(kwargs) diff --git a/ultralytics/yolo/engine/results.py b/ultralytics/yolo/engine/results.py index 7aecd02..1bf2d69 100644 --- a/ultralytics/yolo/engine/results.py +++ b/ultralytics/yolo/engine/results.py @@ -277,6 +277,8 @@ class Masks(SimpleClass): self.masks = masks # N, h, w self.orig_shape = orig_shape + @property + @lru_cache(maxsize=1) def segments(self): # Segments-deprecated (normalized) LOGGER.warning("WARNING ⚠️ 'Masks.segments' is deprecated. Use 'Masks.xyn' for segments (normalized) and " diff --git a/ultralytics/yolo/utils/__init__.py b/ultralytics/yolo/utils/__init__.py index 112a6c2..779d59e 100644 --- a/ultralytics/yolo/utils/__init__.py +++ b/ultralytics/yolo/utils/__init__.py @@ -321,10 +321,13 @@ def is_online() -> bool: bool: True if connection is successful, False otherwise. """ import socket - with contextlib.suppress(Exception): - host = socket.gethostbyname('www.github.com') - socket.create_connection((host, 80), timeout=2) - return True + + for server in '1.1.1.1', '8.8.8.8', '223.5.5.5': # Cloudflare, Google, AliDNS: + try: + socket.create_connection((server, 53), timeout=2) # connect to (server, port=53) + return True + except (socket.timeout, socket.gaierror, OSError): + continue return False @@ -586,7 +589,7 @@ def set_sentry(): logging.getLogger(logger).setLevel(logging.CRITICAL) -def get_settings(file=USER_CONFIG_DIR / 'settings.yaml', version='0.0.2'): +def get_settings(file=USER_CONFIG_DIR / 'settings.yaml', version='0.0.3'): """ Loads a global Ultralytics settings YAML file or creates one with default values if it does not exist. @@ -609,8 +612,9 @@ def get_settings(file=USER_CONFIG_DIR / 'settings.yaml', version='0.0.2'): 'datasets_dir': str(datasets_root / 'datasets'), # default datasets directory. 'weights_dir': str(root / 'weights'), # default weights directory. 'runs_dir': str(root / 'runs'), # default runs directory. - 'sync': True, # sync analytics to help with YOLO development 'uuid': hashlib.sha256(str(uuid.getnode()).encode()).hexdigest(), # anonymized uuid hash + 'sync': True, # sync analytics to help with YOLO development + 'api_key': '', # Ultralytics HUB API key (https://hub.ultralytics.com/) 'settings_version': version} # Ultralytics settings version with torch_distributed_zero_first(RANK): diff --git a/ultralytics/yolo/utils/callbacks/mlflow.py b/ultralytics/yolo/utils/callbacks/mlflow.py index 547ca91..cdfdf44 100644 --- a/ultralytics/yolo/utils/callbacks/mlflow.py +++ b/ultralytics/yolo/utils/callbacks/mlflow.py @@ -25,7 +25,7 @@ def on_pretrain_routine_end(trainer): mlflow_location = os.environ['MLFLOW_TRACKING_URI'] # "http://192.168.xxx.xxx:5000" mlflow.set_tracking_uri(mlflow_location) - experiment_name = trainer.args.project or 'YOLOv8' + experiment_name = trainer.args.project or '/Shared/YOLOv8' experiment = mlflow.get_experiment_by_name(experiment_name) if experiment is None: mlflow.create_experiment(experiment_name) @@ -33,16 +33,15 @@ def on_pretrain_routine_end(trainer): prefix = colorstr('MLFlow: ') try: - run, active_run = mlflow, mlflow.start_run() if mlflow else None - if active_run is not None: - run_id = active_run.info.run_id - LOGGER.info(f'{prefix}Using run_id({run_id}) at {mlflow_location}') + run, active_run = mlflow, mlflow.active_run() + if not active_run: + active_run = mlflow.start_run(experiment_id=experiment.experiment_id) + run_id = active_run.info.run_id + LOGGER.info(f'{prefix}Using run_id({run_id}) at {mlflow_location}') + run.log_params(vars(trainer.model.args)) except Exception as err: LOGGER.error(f'{prefix}Failing init - {repr(err)}') LOGGER.warning(f'{prefix}Continuing without Mlflow') - run = None - - run.log_params(vars(trainer.model.args)) def on_fit_epoch_end(trainer): diff --git a/ultralytics/yolo/utils/checks.py b/ultralytics/yolo/utils/checks.py index cf54ebd..3f41fa9 100644 --- a/ultralytics/yolo/utils/checks.py +++ b/ultralytics/yolo/utils/checks.py @@ -142,7 +142,7 @@ def check_pip_update_available(): bool: True if an update is available, False otherwise. """ if ONLINE and is_pip_package(): - with contextlib.suppress(ConnectionError): + with contextlib.suppress(Exception): from ultralytics import __version__ latest = check_latest_pypi_version() if pkg.parse_version(__version__) < pkg.parse_version(latest): # update is available diff --git a/ultralytics/yolo/utils/downloads.py b/ultralytics/yolo/utils/downloads.py index 25137ec..2caefef 100644 --- a/ultralytics/yolo/utils/downloads.py +++ b/ultralytics/yolo/utils/downloads.py @@ -12,7 +12,7 @@ import requests import torch from tqdm import tqdm -from ultralytics.yolo.utils import LOGGER, checks, is_online +from ultralytics.yolo.utils import LOGGER, checks, emojis, is_online GITHUB_ASSET_NAMES = [f'yolov8{size}{suffix}.pt' for size in 'nsmlx' for suffix in ('', '6', '-cls', '-seg')] + \ [f'yolov5{size}u.pt' for size in 'nsmlx'] + \ @@ -113,9 +113,9 @@ def safe_download(url, f.unlink() # remove partial downloads except Exception as e: if i == 0 and not is_online(): - raise ConnectionError(f'❌ Download failure for {url}. Environment is not online.') from e + raise ConnectionError(emojis(f'❌ Download failure for {url}. Environment is not online.')) from e elif i >= retry: - raise ConnectionError(f'❌ Download failure for {url}. Retry limit reached.') from e + raise ConnectionError(emojis(f'❌ Download failure for {url}. Retry limit reached.')) from e LOGGER.warning(f'⚠️ Download failure, retrying {i + 1}/{retry} {url}...') if unzip and f.exists() and f.suffix in ('.zip', '.tar', '.gz'): diff --git a/ultralytics/yolo/utils/plotting.py b/ultralytics/yolo/utils/plotting.py index 8418495..e40d486 100644 --- a/ultralytics/yolo/utils/plotting.py +++ b/ultralytics/yolo/utils/plotting.py @@ -114,7 +114,7 @@ class Annotator: self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255 if im_gpu.device != masks.device: im_gpu = im_gpu.to(masks.device) - colors = torch.tensor(colors, device=masks.device, dtype=torch.float32) / 255.0 + colors = torch.tensor(colors, device=masks.device, dtype=torch.float32) / 255.0 # shape(n,3) colors = colors[:, None, None] # shape(n,1,1,3) masks = masks.unsqueeze(3) # shape(n,h,w,1) masks_color = masks * (colors * alpha) # shape(n,h,w,3) diff --git a/ultralytics/yolo/v8/segment/predict.py b/ultralytics/yolo/v8/segment/predict.py index 90211c1..bc5c168 100644 --- a/ultralytics/yolo/v8/segment/predict.py +++ b/ultralytics/yolo/v8/segment/predict.py @@ -78,7 +78,7 @@ class SegmentationPredictor(DetectionPredictor): for j, d in enumerate(reversed(det)): c, conf, id = int(d.cls), float(d.conf), None if d.id is None else int(d.id.item()) if self.args.save_txt: # Write to file - seg = mask.segments[len(det) - j - 1].copy().reshape(-1) # reversed mask.segments, (n,2) to (n*2) + seg = mask.xyn[len(det) - j - 1].copy().reshape(-1) # reversed mask.xyn, (n,2) to (n*2) line = (c, *seg) + (conf, ) * self.args.save_conf + (() if id is None else (id, )) with open(f'{self.txt_path}.txt', 'a') as f: f.write(('%g ' * len(line)).rstrip() % line + '\n')