diff --git a/README.md b/README.md index f922504..40eb0d2 100644 --- a/README.md +++ b/README.md @@ -183,12 +183,12 @@ See [Pose Docs](https://docs.ultralytics.com/tasks/) for usage examples with the | Model | size
(pixels) | mAPbox
50-95 | mAPpose
50-95 | Speed
CPU ONNX
(ms) | Speed
A100 TensorRT
(ms) | params
(M) | FLOPs
(B) | | ---------------------------------------------------------------------------------------------------- | --------------------- | -------------------- | --------------------- | ------------------------------ | ----------------------------------- | ------------------ | ----------------- | -| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | - | - | 3.3 | 9.2 | -| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | - | - | 11.6 | 30.2 | -| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | - | - | 26.4 | 81.0 | -| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | - | - | 44.4 | 168.6 | -| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | - | - | 69.4 | 263.2 | -| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | - | - | 99.1 | 1066.4 | +| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | 131.8 | 1.18 | 3.3 | 9.2 | +| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | 233.2 | 1.42 | 11.6 | 30.2 | +| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | 456.3 | 2.00 | 26.4 | 81.0 | +| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | 784.5 | 2.59 | 44.4 | 168.6 | +| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | 1607.1 | 3.73 | 69.4 | 263.2 | +| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | 4088.7 | 10.04 | 99.1 | 1066.4 | - **mAPval** values are for single-model single-scale on [COCO Keypoints val2017](http://cocodataset.org) dataset. @@ -214,16 +214,16 @@ See [Pose Docs](https://docs.ultralytics.com/tasks/) for usage examples with the - + -| Roboflow | ClearML ⭐ NEW | Comet ⭐ NEW | Neural Magic ⭐ NEW | -| :--------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: | -| Label and export your custom datasets directly to YOLOv8 for training with [Roboflow](https://roboflow.com/?ref=ultralytics) | Automatically track, visualize and even remotely train YOLOv8 using [ClearML](https://cutt.ly/yolov5-readme-clearml) (open-source!) | Free forever, [Comet](https://bit.ly/yolov5-readme-comet2) lets you save YOLOv8 models, resume training, and interactively visualize and debug predictions | Run YOLOv8 inference up to 6x faster with [Neural Magic DeepSparse](https://bit.ly/yolov5-neuralmagic) | +| Roboflow | ClearML ⭐ NEW | Comet ⭐ NEW | Neural Magic ⭐ NEW | +| :--------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: | +| Label and export your custom datasets directly to YOLOv8 for training with [Roboflow](https://roboflow.com/?ref=ultralytics) | Automatically track, visualize and even remotely train YOLOv8 using [ClearML](https://cutt.ly/yolov5-readme-clearml) (open-source!) | Free forever, [Comet](https://bit.ly/yolov8-readme-comet) lets you save YOLOv8 models, resume training, and interactively visualize and debug predictions | Run YOLOv8 inference up to 6x faster with [Neural Magic DeepSparse](https://bit.ly/yolov5-neuralmagic) | ##
Ultralytics HUB
diff --git a/README.zh-CN.md b/README.zh-CN.md index b709b3d..bf7f0ef 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -169,12 +169,12 @@ See [Pose Docs](https://docs.ultralytics.com/tasks/) for usage examples with the | Model | size
(pixels) | mAPbox
50-95 | mAPpose
50-95 | Speed
CPU ONNX
(ms) | Speed
A100 TensorRT
(ms) | params
(M) | FLOPs
(B) | | ---------------------------------------------------------------------------------------------------- | --------------------- | -------------------- | --------------------- | ------------------------------ | ----------------------------------- | ------------------ | ----------------- | -| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | - | - | 3.3 | 9.2 | -| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | - | - | 11.6 | 30.2 | -| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | - | - | 26.4 | 81.0 | -| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | - | - | 44.4 | 168.6 | -| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | - | - | 69.4 | 263.2 | -| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | - | - | 99.1 | 1066.4 | +| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | 131.8 | 1.18 | 3.3 | 9.2 | +| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | 233.2 | 1.42 | 11.6 | 30.2 | +| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | 456.3 | 2.00 | 26.4 | 81.0 | +| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | 784.5 | 2.59 | 44.4 | 168.6 | +| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | 1607.1 | 3.73 | 69.4 | 263.2 | +| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | 4088.7 | 10.04 | 99.1 | 1066.4 | - **mAPval** values are for single-model single-scale on [COCO Keypoints val2017](http://cocodataset.org) dataset. @@ -200,16 +200,16 @@ See [Pose Docs](https://docs.ultralytics.com/tasks/) for usage examples with the - + -| Roboflow | ClearML ⭐ 新 | Comet ⭐ 新 | Neural Magic ⭐ 新 | -| :--------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: | -| 将您的自定义数据集进行标注并直接导出到 YOLOv8 以进行训练 [Roboflow](https://roboflow.com/?ref=ultralytics) | 自动跟踪、可视化甚至远程训练 YOLOv8 [ClearML](https://cutt.ly/yolov5-readme-clearml)(开源!) | 永远免费,[Comet](https://bit.ly/yolov5-readme-comet2)可让您保存 YOLOv8 模型、恢复训练以及交互式可视化和调试预测 | 使用 [Neural Magic DeepSparse](https://bit.ly/yolov5-neuralmagic),运行 YOLOv8 推理的速度最高可提高6倍 | +| Roboflow | ClearML ⭐ 新 | Comet ⭐ 新 | Neural Magic ⭐ 新 | +| :--------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: | +| 将您的自定义数据集进行标注并直接导出到 YOLOv8 以进行训练 [Roboflow](https://roboflow.com/?ref=ultralytics) | 自动跟踪、可视化甚至远程训练 YOLOv8 [ClearML](https://cutt.ly/yolov5-readme-clearml)(开源!) | 永远免费,[Comet](https://bit.ly/yolov8-readme-comet)可让您保存 YOLOv8 模型、恢复训练以及交互式可视化和调试预测 | 使用 [Neural Magic DeepSparse](https://bit.ly/yolov5-neuralmagic),运行 YOLOv8 推理的速度最高可提高6倍 | ##
Ultralytics HUB
diff --git a/docs/tasks/pose.md b/docs/tasks/pose.md index e32bc7d..1510a9f 100644 --- a/docs/tasks/pose.md +++ b/docs/tasks/pose.md @@ -25,12 +25,12 @@ Ultralytics [release](https://github.com/ultralytics/assets/releases) on first u | Model | size
(pixels) | mAPbox
50-95 | mAPpose
50-95 | Speed
CPU ONNX
(ms) | Speed
A100 TensorRT
(ms) | params
(M) | FLOPs
(B) | |------------------------------------------------------------------------------------------------------|-----------------------|----------------------|-----------------------|--------------------------------|-------------------------------------|--------------------|-------------------| -| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | - | - | 3.3 | 9.2 | -| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | - | - | 11.6 | 30.2 | -| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | - | - | 26.4 | 81.0 | -| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | - | - | 44.4 | 168.6 | -| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | - | - | 69.4 | 263.2 | -| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | - | - | 99.1 | 1066.4 | +| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | 131.8 | 1.18 | 3.3 | 9.2 | +| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | 233.2 | 1.42 | 11.6 | 30.2 | +| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | 456.3 | 2.00 | 26.4 | 81.0 | +| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | 784.5 | 2.59 | 44.4 | 168.6 | +| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | 1607.1 | 3.73 | 69.4 | 263.2 | +| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | 4088.7 | 10.04 | 99.1 | 1066.4 | - **mAPval** values are for single-model single-scale on [COCO Keypoints val2017](http://cocodataset.org) dataset. @@ -123,7 +123,7 @@ Use a trained YOLOv8n-pose model to run predictions on images. === "CLI" ```bash - yolo pose predict model=yolov8n.pt source='https://ultralytics.com/images/bus.jpg' # predict with official model + yolo pose predict model=yolov8n-pose.pt source='https://ultralytics.com/images/bus.jpg' # predict with official model yolo pose predict model=path/to/best.pt source='https://ultralytics.com/images/bus.jpg' # predict with custom model ``` @@ -131,7 +131,7 @@ See full `predict` mode details in the [Predict](https://docs.ultralytics.com/mo ## Export -Export a YOLOv8n model to a different format like ONNX, CoreML, etc. +Export a YOLOv8n Pose model to a different format like ONNX, CoreML, etc. !!! example "" @@ -141,7 +141,7 @@ Export a YOLOv8n model to a different format like ONNX, CoreML, etc. from ultralytics import YOLO # Load a model - model = YOLO('yolov8n.pt') # load an official model + model = YOLO('yolov8n-pose.pt') # load an official model model = YOLO('path/to/best.pt') # load a custom trained # Export the model @@ -150,7 +150,7 @@ Export a YOLOv8n model to a different format like ONNX, CoreML, etc. === "CLI" ```bash - yolo export model=yolov8n.pt format=onnx # export official model + yolo export model=yolov8n-pose.pt format=onnx # export official model yolo export model=path/to/best.pt format=onnx # export custom trained model ``` diff --git a/tests/test_engine.py b/tests/test_engine.py index c20edc1..90dc7b9 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -2,7 +2,9 @@ from pathlib import Path +from ultralytics import YOLO from ultralytics.yolo.cfg import get_cfg +from ultralytics.yolo.engine.exporter import Exporter from ultralytics.yolo.utils import DEFAULT_CFG, ROOT, SETTINGS from ultralytics.yolo.v8 import classify, detect, segment @@ -14,20 +16,38 @@ MODEL = Path(SETTINGS['weights_dir']) / 'yolov8n' SOURCE = ROOT / 'assets' +def test_func(model=None): + print('callback test passed') + + +def test_export(): + exporter = Exporter() + exporter.add_callback('on_export_start', test_func) + assert test_func in exporter.callbacks['on_export_start'], 'callback test failed' + f = exporter(model=YOLO(CFG_DET).model) + YOLO(f)(SOURCE) # exported model inference + + def test_detect(): overrides = {'data': 'coco8.yaml', 'model': CFG_DET, 'imgsz': 32, 'epochs': 1, 'save': False} CFG.data = 'coco8.yaml' # Trainer trainer = detect.DetectionTrainer(overrides=overrides) + trainer.add_callback('on_train_start', test_func) + assert test_func in trainer.callbacks['on_train_start'], 'callback test failed' trainer.train() # Validator val = detect.DetectionValidator(args=CFG) + val.add_callback('on_val_start', test_func) + assert test_func in val.callbacks['on_val_start'], 'callback test failed' val(model=trainer.best) # validate best.pt # Predictor pred = detect.DetectionPredictor(overrides={'imgsz': [64, 64]}) + pred.add_callback('on_predict_start', test_func) + assert test_func in pred.callbacks['on_predict_start'], 'callback test failed' result = pred(source=SOURCE, model=f'{MODEL}.pt') assert len(result), 'predictor test failed' @@ -50,14 +70,20 @@ def test_segment(): # trainer trainer = segment.SegmentationTrainer(overrides=overrides) + trainer.add_callback('on_train_start', test_func) + assert test_func in trainer.callbacks['on_train_start'], 'callback test failed' trainer.train() # Validator val = segment.SegmentationValidator(args=CFG) + val.add_callback('on_val_start', test_func) + assert test_func in val.callbacks['on_val_start'], 'callback test failed' val(model=trainer.best) # validate best.pt # Predictor pred = segment.SegmentationPredictor(overrides={'imgsz': [64, 64]}) + pred.add_callback('on_predict_start', test_func) + assert test_func in pred.callbacks['on_predict_start'], 'callback test failed' result = pred(source=SOURCE, model=f'{MODEL}-seg.pt') assert len(result), 'predictor test failed' @@ -81,13 +107,19 @@ def test_classify(): # Trainer trainer = classify.ClassificationTrainer(overrides=overrides) + trainer.add_callback('on_train_start', test_func) + assert test_func in trainer.callbacks['on_train_start'], 'callback test failed' trainer.train() # Validator val = classify.ClassificationValidator(args=CFG) + val.add_callback('on_val_start', test_func) + assert test_func in val.callbacks['on_val_start'], 'callback test failed' val(model=trainer.best) # Predictor pred = classify.ClassificationPredictor(overrides={'imgsz': [64, 64]}) + pred.add_callback('on_predict_start', test_func) + assert test_func in pred.callbacks['on_predict_start'], 'callback test failed' result = pred(source=SOURCE, model=trainer.best) assert len(result), 'predictor test failed' diff --git a/ultralytics/__init__.py b/ultralytics/__init__.py index 496d5fd..a65e065 100644 --- a/ultralytics/__init__.py +++ b/ultralytics/__init__.py @@ -1,6 +1,6 @@ # Ultralytics YOLO 🚀, GPL-3.0 license -__version__ = '8.0.66' +__version__ = '8.0.67' from ultralytics.hub import start from ultralytics.yolo.engine.model import YOLO diff --git a/ultralytics/hub/__init__.py b/ultralytics/hub/__init__.py index 15133ba..40ea503 100644 --- a/ultralytics/hub/__init__.py +++ b/ultralytics/hub/__init__.py @@ -3,7 +3,7 @@ import requests from ultralytics.hub.utils import PREFIX, split_key -from ultralytics.yolo.utils import LOGGER +from ultralytics.yolo.utils import LOGGER, SETTINGS, USER_CONFIG_DIR, yaml_save def login(api_key=''): @@ -15,7 +15,7 @@ def login(api_key=''): Example: from ultralytics import hub - hub.login('your_api_key') + hub.login('API_KEY') """ from ultralytics.hub.auth import Auth Auth(api_key) @@ -23,13 +23,15 @@ def login(api_key=''): def logout(): """ - Logout Ultralytics HUB + Log out of Ultralytics HUB by removing the API key from the settings file. To log in again, use 'yolo hub login'. Example: from ultralytics import hub hub.logout() """ - LOGGER.warning('WARNING ⚠️ This method is not yet implemented.') + SETTINGS['api_key'] = '' + yaml_save(USER_CONFIG_DIR / 'settings.yaml', SETTINGS) + LOGGER.info(f"{PREFIX}logged out ✅. To log in again, use 'yolo hub login'.") def start(key=''): diff --git a/ultralytics/models/README.md b/ultralytics/models/README.md index e4d9da1..ddf64d5 100644 --- a/ultralytics/models/README.md +++ b/ultralytics/models/README.md @@ -89,12 +89,12 @@ Available Models: | Model | size
(pixels) | mAPbox
50-95 | mAPpose
50-95 | Speed
CPU ONNX
(ms) | Speed
A100 TensorRT
(ms) | params
(M) | FLOPs
(B) | | ---------------------------------------------------------------------------------------------------- | --------------------- | -------------------- | --------------------- | ------------------------------ | ----------------------------------- | ------------------ | ----------------- | -| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | - | - | 3.3 | 9.2 | -| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | - | - | 11.6 | 30.2 | -| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | - | - | 26.4 | 81.0 | -| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | - | - | 44.4 | 168.6 | -| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | - | - | 69.4 | 263.2 | -| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | - | - | 99.1 | 1066.4 | +| [YOLOv8n-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-pose.pt) | 640 | - | 49.7 | 131.8 | 1.18 | 3.3 | 9.2 | +| [YOLOv8s-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s-pose.pt) | 640 | - | 59.2 | 233.2 | 1.42 | 11.6 | 30.2 | +| [YOLOv8m-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m-pose.pt) | 640 | - | 63.6 | 456.3 | 2.00 | 26.4 | 81.0 | +| [YOLOv8l-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8l-pose.pt) | 640 | - | 67.0 | 784.5 | 2.59 | 44.4 | 168.6 | +| [YOLOv8x-pose](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose.pt) | 640 | - | 68.9 | 1607.1 | 3.73 | 69.4 | 263.2 | +| [YOLOv8x-pose-p6](https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8x-pose-p6.pt) | 1280 | - | 71.5 | 4088.7 | 10.04 | 99.1 | 1066.4 | diff --git a/ultralytics/yolo/data/utils.py b/ultralytics/yolo/data/utils.py index d62bb8b..ffa4f3d 100644 --- a/ultralytics/yolo/data/utils.py +++ b/ultralytics/yolo/data/utils.py @@ -17,7 +17,7 @@ from PIL import ExifTags, Image, ImageOps from tqdm import tqdm from ultralytics.nn.autobackend import check_class_names -from ultralytics.yolo.utils import DATASETS_DIR, LOGGER, NUM_THREADS, ROOT, colorstr, emojis, yaml_load +from ultralytics.yolo.utils import DATASETS_DIR, LOGGER, NUM_THREADS, ROOT, clean_url, colorstr, emojis, yaml_load from ultralytics.yolo.utils.checks import check_file, check_font, is_ascii from ultralytics.yolo.utils.downloads import download, safe_download, unzip_file from ultralytics.yolo.utils.ops import segments2boxes @@ -241,7 +241,7 @@ def check_det_dataset(dataset, autodownload=True): if val: val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path if not all(x.exists() for x in val): - name = str(dataset).split('?')[0] # dataset name with URL auth stripped + name = clean_url(dataset) # dataset name with URL auth stripped m = f"\nDataset '{name}' images not found ⚠️, missing paths %s" % [str(x) for x in val if not x.exists()] if s and autodownload: LOGGER.warning(m) diff --git a/ultralytics/yolo/engine/exporter.py b/ultralytics/yolo/engine/exporter.py index a8cdf3a..e0beb90 100644 --- a/ultralytics/yolo/engine/exporter.py +++ b/ultralytics/yolo/engine/exporter.py @@ -53,7 +53,6 @@ import platform import subprocess import time import warnings -from collections import defaultdict from copy import deepcopy from pathlib import Path @@ -130,7 +129,7 @@ class Exporter: save_dir (Path): Directory to save results. """ - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): """ Initializes the Exporter class. @@ -139,7 +138,7 @@ class Exporter: overrides (dict, optional): Configuration overrides. Defaults to None. """ self.args = get_cfg(cfg, overrides) - self.callbacks = defaultdict(list, callbacks.default_callbacks) # add callbacks + self.callbacks = _callbacks if _callbacks else callbacks.get_default_callbacks() callbacks.add_integration_callbacks(self) @smart_inference_mode() @@ -854,6 +853,12 @@ class Exporter: LOGGER.info(f'{prefix} pipeline success') return model + def add_callback(self, event: str, callback): + """ + Appends the given callback. + """ + self.callbacks[event].append(callback) + def run_callbacks(self, event: str): for callback in self.callbacks.get(event, []): callback(self) diff --git a/ultralytics/yolo/engine/model.py b/ultralytics/yolo/engine/model.py index aea3661..88103a2 100644 --- a/ultralytics/yolo/engine/model.py +++ b/ultralytics/yolo/engine/model.py @@ -78,7 +78,7 @@ class YOLO: task (Any, optional): Task type for the YOLO model. Defaults to None. """ - self._reset_callbacks() + self.callbacks = callbacks.get_default_callbacks() self.predictor = None # reuse predictor self.model = None # model object self.trainer = None # trainer object @@ -238,7 +238,7 @@ class YOLO: overrides['save'] = kwargs.get('save', False) # not save files by default if not self.predictor: self.task = overrides.get('task') or self.task - self.predictor = TASK_MAP[self.task][3](overrides=overrides) + self.predictor = TASK_MAP[self.task][3](overrides=overrides, _callbacks=self.callbacks) self.predictor.setup_model(model=self.model, verbose=is_cli) else: # only update args if predictor is already setup self.predictor.args = get_cfg(self.predictor.args, overrides) @@ -277,7 +277,7 @@ class YOLO: args.imgsz = self.model.args['imgsz'] # use trained imgsz unless custom value is passed args.imgsz = check_imgsz(args.imgsz, max_dim=1) - validator = TASK_MAP[self.task][2](args=args) + validator = TASK_MAP[self.task][2](args=args, _callbacks=self.callbacks) validator(model=self.model) self.metrics = validator.metrics @@ -316,7 +316,7 @@ class YOLO: args.imgsz = self.model.args['imgsz'] # use trained imgsz unless custom value is passed if args.batch == DEFAULT_CFG.batch: args.batch = 1 # default to 1 if not modified - return Exporter(overrides=args)(model=self.model) + return Exporter(overrides=args, _callbacks=self.callbacks)(model=self.model) def train(self, **kwargs): """ @@ -344,7 +344,7 @@ class YOLO: overrides['resume'] = self.ckpt_path self.task = overrides.get('task') or self.task - self.trainer = TASK_MAP[self.task][1](overrides=overrides) + self.trainer = TASK_MAP[self.task][1](overrides=overrides, _callbacks=self.callbacks) if not overrides.get('resume'): # manually set model only if not resuming self.trainer.model = self.trainer.get_model(weights=self.model if self.ckpt else None, cfg=self.model.yaml) self.model = self.trainer.model @@ -387,19 +387,17 @@ class YOLO: """ return self.model.transforms if hasattr(self.model, 'transforms') else None - @staticmethod - def add_callback(event: str, func): + def add_callback(self, event: str, func): """ Add callback """ - callbacks.default_callbacks[event].append(func) + self.callbacks[event].append(func) @staticmethod def _reset_ckpt_args(args): include = {'imgsz', 'data', 'task', 'single_cls'} # only remember these arguments when loading a PyTorch model return {k: v for k, v in args.items() if k in include} - @staticmethod - def _reset_callbacks(): + def _reset_callbacks(self): for event in callbacks.default_callbacks.keys(): - callbacks.default_callbacks[event] = [callbacks.default_callbacks[event][0]] + self.callbacks[event] = [callbacks.default_callbacks[event][0]] diff --git a/ultralytics/yolo/engine/predictor.py b/ultralytics/yolo/engine/predictor.py index f511459..d7ee374 100644 --- a/ultralytics/yolo/engine/predictor.py +++ b/ultralytics/yolo/engine/predictor.py @@ -28,7 +28,6 @@ Usage - formats: yolov8n_paddle_model # PaddlePaddle """ import platform -from collections import defaultdict from pathlib import Path import cv2 @@ -75,7 +74,7 @@ class BasePredictor: data_path (str): Path to data. """ - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): """ Initializes the BasePredictor class. @@ -104,7 +103,7 @@ class BasePredictor: self.data_path = None self.source_type = None self.batch = None - self.callbacks = defaultdict(list, callbacks.default_callbacks) # add callbacks + self.callbacks = _callbacks if _callbacks else callbacks.get_default_callbacks() callbacks.add_integration_callbacks(self) def preprocess(self, img): @@ -283,3 +282,9 @@ class BasePredictor: def run_callbacks(self, event: str): for callback in self.callbacks.get(event, []): callback(self) + + def add_callback(self, event: str, func): + """ + Add callback + """ + self.callbacks[event].append(func) diff --git a/ultralytics/yolo/engine/trainer.py b/ultralytics/yolo/engine/trainer.py index fc12eda..5f17a4d 100644 --- a/ultralytics/yolo/engine/trainer.py +++ b/ultralytics/yolo/engine/trainer.py @@ -8,7 +8,6 @@ Usage: import os import subprocess import time -from collections import defaultdict from copy import deepcopy from datetime import datetime from pathlib import Path @@ -26,7 +25,7 @@ from ultralytics.nn.tasks import attempt_load_one_weight, attempt_load_weights from ultralytics.yolo.cfg import get_cfg from ultralytics.yolo.data.utils import check_cls_dataset, check_det_dataset from ultralytics.yolo.utils import (DEFAULT_CFG, LOGGER, ONLINE, RANK, ROOT, SETTINGS, TQDM_BAR_FORMAT, __version__, - callbacks, colorstr, emojis, yaml_save) + callbacks, clean_url, colorstr, emojis, yaml_save) from ultralytics.yolo.utils.autobatch import check_train_batch_size from ultralytics.yolo.utils.checks import check_file, check_imgsz, print_args from ultralytics.yolo.utils.dist import ddp_cleanup, generate_ddp_command @@ -72,7 +71,7 @@ class BaseTrainer: csv (Path): Path to results CSV file. """ - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): """ Initializes the BaseTrainer class. @@ -124,7 +123,7 @@ class BaseTrainer: if 'yaml_file' in self.data: self.args.data = self.data['yaml_file'] # for validating 'yolo train data=url.zip' usage except Exception as e: - raise RuntimeError(emojis(f"Dataset '{self.args.data}' error ❌ {e}")) from e + raise RuntimeError(emojis(f"Dataset '{clean_url(self.args.data)}' error ❌ {e}")) from e self.trainset, self.testset = self.get_dataset(self.data) self.ema = None @@ -143,7 +142,7 @@ class BaseTrainer: self.plot_idx = [0, 1, 2] # Callbacks - self.callbacks = defaultdict(list, callbacks.default_callbacks) # add callbacks + self.callbacks = _callbacks if _callbacks else callbacks.get_default_callbacks() if RANK in (-1, 0): callbacks.add_integration_callbacks(self) diff --git a/ultralytics/yolo/engine/validator.py b/ultralytics/yolo/engine/validator.py index dddca2a..6eb82f9 100644 --- a/ultralytics/yolo/engine/validator.py +++ b/ultralytics/yolo/engine/validator.py @@ -19,7 +19,6 @@ Usage - formats: yolov8n_paddle_model # PaddlePaddle """ import json -from collections import defaultdict from pathlib import Path import torch @@ -55,7 +54,7 @@ class BaseValidator: save_dir (Path): Directory to save results. """ - def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None): + def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None): """ Initializes a BaseValidator instance. @@ -85,7 +84,7 @@ class BaseValidator: if self.args.conf is None: self.args.conf = 0.001 # default conf=0.001 - self.callbacks = defaultdict(list, callbacks.default_callbacks) # add callbacks + self.callbacks = _callbacks if _callbacks else callbacks.get_default_callbacks() @smart_inference_mode() def __call__(self, trainer=None, model=None): @@ -195,6 +194,12 @@ class BaseValidator: LOGGER.info(f"Results saved to {colorstr('bold', self.save_dir)}") return stats + def add_callback(self, event: str, callback): + """ + Appends the given callback. + """ + self.callbacks[event].append(callback) + def run_callbacks(self, event: str): for callback in self.callbacks.get(event, []): callback(self) diff --git a/ultralytics/yolo/utils/__init__.py b/ultralytics/yolo/utils/__init__.py index 5d5c562..4c527a4 100644 --- a/ultralytics/yolo/utils/__init__.py +++ b/ultralytics/yolo/utils/__init__.py @@ -10,6 +10,7 @@ import subprocess import sys import tempfile import threading +import urllib import uuid from pathlib import Path from types import SimpleNamespace @@ -165,7 +166,7 @@ class IterableSimpleNamespace(SimpleNamespace): def set_logging(name=LOGGING_NAME, verbose=True): # sets up logging for the given name 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({ 'version': 1, 'disable_existing_loggers': False, @@ -649,10 +650,20 @@ def set_settings(kwargs, file=USER_CONFIG_DIR / 'settings.yaml'): def deprecation_warn(arg, new_arg, version=None): if not version: - version = float(__version__[0:3]) + 0.2 # deprecate after 2nd major release - LOGGER.warning( - f'WARNING: `{arg}` is deprecated and will be removed in upcoming major release {version}. Use `{new_arg}` instead' - ) + version = float(__version__[:3]) + 0.2 # deprecate after 2nd major release + LOGGER.warning(f"WARNING ⚠️ '{arg}' is deprecated and will be removed in 'ultralytics {version}' in the future. " + f"Please use '{new_arg}' instead.") + + +def clean_url(url): + # Strip auth from URL, i.e. https://url.com/file.txt?auth -> https://url.com/file.txt + url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/ + return urllib.parse.unquote(url).split('?')[0] # '%2F' to '/', split https://url.com/file.txt?auth + + +def url2file(url): + # Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt + return Path(clean_url(url)).name # Run below code on yolo/utils init ------------------------------------------------------------------------------------ diff --git a/ultralytics/yolo/utils/callbacks/__init__.py b/ultralytics/yolo/utils/callbacks/__init__.py index fb5dfe2..1071ef4 100644 --- a/ultralytics/yolo/utils/callbacks/__init__.py +++ b/ultralytics/yolo/utils/callbacks/__init__.py @@ -1,3 +1,3 @@ -from .base import add_integration_callbacks, default_callbacks +from .base import add_integration_callbacks, default_callbacks, get_default_callbacks -__all__ = 'add_integration_callbacks', 'default_callbacks' +__all__ = 'add_integration_callbacks', 'default_callbacks', 'get_default_callbacks' diff --git a/ultralytics/yolo/utils/callbacks/base.py b/ultralytics/yolo/utils/callbacks/base.py index c5a843f..0779594 100644 --- a/ultralytics/yolo/utils/callbacks/base.py +++ b/ultralytics/yolo/utils/callbacks/base.py @@ -2,6 +2,8 @@ """ Base callbacks """ +from collections import defaultdict +from copy import deepcopy # Trainer callbacks ---------------------------------------------------------------------------------------------------- @@ -143,6 +145,10 @@ default_callbacks = { 'on_export_end': [on_export_end]} +def get_default_callbacks(): + return defaultdict(list, deepcopy(default_callbacks)) + + def add_integration_callbacks(instance): from .clearml import callbacks as clearml_callbacks from .comet import callbacks as comet_callbacks diff --git a/ultralytics/yolo/utils/callbacks/clearml.py b/ultralytics/yolo/utils/callbacks/clearml.py index 094763b..bc73a12 100644 --- a/ultralytics/yolo/utils/callbacks/clearml.py +++ b/ultralytics/yolo/utils/callbacks/clearml.py @@ -1,10 +1,17 @@ # Ultralytics YOLO 🚀, GPL-3.0 license +import re + +import matplotlib.image as mpimg +import matplotlib.pyplot as plt + from ultralytics.yolo.utils import LOGGER, TESTS_RUNNING from ultralytics.yolo.utils.torch_utils import get_flops, get_num_params try: import clearml from clearml import Task + from clearml.binding.frameworks.pytorch_bind import PatchPyTorchModelIO + from clearml.binding.matplotlib_bind import PatchedMatplotlib assert hasattr(clearml, '__version__') # verify package is not directory assert not TESTS_RUNNING # do not log pytest @@ -12,21 +19,61 @@ except (ImportError, AssertionError): clearml = None -def _log_images(imgs_dict, group='', step=0): - task = Task.current_task() - if task: - for k, v in imgs_dict.items(): - task.get_logger().report_image(group, k, step, v) +def _log_debug_samples(files, title='Debug Samples'): + """ + Log files (images) as debug samples in the ClearML task. + + arguments: + files (List(PosixPath)) a list of file paths in PosixPath format + title (str) A title that groups together images with the same values + """ + for f in files: + if f.exists(): + it = re.search(r'_batch(\d+)', f.name) + iteration = int(it.groups()[0]) if it else 0 + Task.current_task().get_logger().report_image(title=title, + series=f.name.replace(it.group(), ''), + local_path=str(f), + iteration=iteration) + + +def _log_plot(title, plot_path): + """ + Log image as plot in the plot section of ClearML + + arguments: + title (str) Title of the plot + plot_path (PosixPath or str) Path to the saved image file + """ + img = mpimg.imread(plot_path) + fig = plt.figure() + ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect='auto', xticks=[], yticks=[]) # no ticks + ax.imshow(img) + + Task.current_task().get_logger().report_matplotlib_figure(title, '', figure=fig, report_interactive=False) def on_pretrain_routine_start(trainer): + # TODO: reuse existing task try: - task = Task.init(project_name=trainer.args.project or 'YOLOv8', - task_name=trainer.args.name, - tags=['YOLOv8'], - output_uri=True, - reuse_last_task_id=False, - auto_connect_frameworks={'pytorch': False}) + if Task.current_task(): + task = Task.current_task() + + # Make sure the automatic pytorch and matplotlib bindings are disabled! + # We are logging these plots and model files manually in the integration + PatchPyTorchModelIO.update_current_task(None) + PatchedMatplotlib.update_current_task(None) + else: + task = Task.init(project_name=trainer.args.project or 'YOLOv8', + task_name=trainer.args.name, + tags=['YOLOv8'], + output_uri=True, + reuse_last_task_id=False, + auto_connect_frameworks={ + 'pytorch': False, + 'matplotlib': False}) + LOGGER.warning('ClearML Initialized a new task. If you want to run remotely, ' + 'please add clearml-init and connect your arguments before initializing YOLO.') task.connect(vars(trainer.args), name='General') except Exception as e: LOGGER.warning(f'WARNING ⚠️ ClearML installed but not initialized correctly, not logging this run. {e}') @@ -34,27 +81,47 @@ def on_pretrain_routine_start(trainer): def on_train_epoch_end(trainer): if trainer.epoch == 1: - _log_images({f.stem: str(f) for f in trainer.save_dir.glob('train_batch*.jpg')}, 'Mosaic', trainer.epoch) + _log_debug_samples(sorted(trainer.save_dir.glob('train_batch*.jpg')), 'Mosaic') def on_fit_epoch_end(trainer): - task = Task.current_task() - if task and trainer.epoch == 0: + # You should have access to the validation bboxes under jdict + Task.current_task().get_logger().report_scalar(title='Epoch Time', + series='Epoch Time', + value=trainer.epoch_time, + iteration=trainer.epoch) + if trainer.epoch == 0: model_info = { 'model/parameters': get_num_params(trainer.model), 'model/GFLOPs': round(get_flops(trainer.model), 3), 'model/speed(ms)': round(trainer.validator.speed['inference'], 3)} - task.connect(model_info, name='Model') + for k, v in model_info.items(): + Task.current_task().get_logger().report_single_value(k, v) + + +def on_val_end(validator): + # Log val_labels and val_pred + _log_debug_samples(sorted(validator.save_dir.glob('val*.jpg')), 'Validation') def on_train_end(trainer): - task = Task.current_task() - if task: - task.update_output_model(model_path=str(trainer.best), model_name=trainer.args.name, auto_delete_file=False) + # Log final results, CM matrix + PR plots + files = ['results.png', 'confusion_matrix.png', *(f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R'))] + files = [(trainer.save_dir / f) for f in files if (trainer.save_dir / f).exists()] # filter + for f in files: + _log_plot(title=f.stem, plot_path=f) + # Report final metrics + for k, v in trainer.validator.metrics.results_dict.items(): + Task.current_task().get_logger().report_single_value(k, v) + # Log the final model + Task.current_task().update_output_model(model_path=str(trainer.best), + model_name=trainer.args.name, + auto_delete_file=False) callbacks = { 'on_pretrain_routine_start': on_pretrain_routine_start, 'on_train_epoch_end': on_train_epoch_end, 'on_fit_epoch_end': on_fit_epoch_end, + 'on_val_end': on_val_end, 'on_train_end': on_train_end} if clearml else {} diff --git a/ultralytics/yolo/utils/callbacks/comet.py b/ultralytics/yolo/utils/callbacks/comet.py index 7c0bd2b..de07427 100644 --- a/ultralytics/yolo/utils/callbacks/comet.py +++ b/ultralytics/yolo/utils/callbacks/comet.py @@ -1,5 +1,8 @@ # Ultralytics YOLO 🚀, GPL-3.0 license -from ultralytics.yolo.utils import LOGGER, TESTS_RUNNING +import os +from pathlib import Path + +from ultralytics.yolo.utils import LOGGER, RANK, TESTS_RUNNING, ops from ultralytics.yolo.utils.torch_utils import get_flops, get_num_params try: @@ -10,41 +13,308 @@ try: except (ImportError, AssertionError): comet_ml = None +COMET_MODE = os.getenv('COMET_MODE', 'online') +COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'YOLOv8') +# 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)) +# determines whether to log confusion matrix every evaluation epoch +COMET_EVAL_LOG_CONFUSION_MATRIX = (os.getenv('COMET_EVAL_LOG_CONFUSION_MATRIX', 'true').lower() == 'true') +# 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_MAX_IMAGE_PREDICTIONS = int(os.getenv('COMET_MAX_IMAGE_PREDICTIONS', 100)) -def on_pretrain_routine_start(trainer): +# ensures certain logging functions only run for supported tasks +COMET_SUPPORTED_TASKS = ['detect'] +# scales reported confidence scores (0.0-1.0) by this value +COMET_MAX_CONFIDENCE_SCORE = int(os.getenv('COMET_MAX_CONFIDENCE_SCORE', 100)) + +# names of plots created by YOLOv8 that are logged to Comet +EVALUATION_PLOT_NAMES = 'F1_curve', 'P_curve', 'R_curve', 'PR_curve', 'confusion_matrix' +LABEL_PLOT_NAMES = 'labels', 'labels_correlogram' + +_comet_image_prediction_count = 0 + + +def _get_experiment_type(mode, project_name): + if mode == 'offline': + return comet_ml.OfflineExperiment(project_name=project_name) + + return comet_ml.Experiment(project_name=project_name) + + +def _create_experiment(args): + # Ensures that the experiment object is only created in a single process during distributed training. + if RANK not in (-1, 0): + return try: - experiment = comet_ml.Experiment(project_name=trainer.args.project or 'YOLOv8') - experiment.set_name(trainer.args.name) - experiment.log_parameters(vars(trainer.args)) + experiment = _get_experiment_type(COMET_MODE, args.project) + experiment.log_parameters(vars(args)) + experiment.log_others({ + 'eval_batch_logging_interval': COMET_EVAL_BATCH_LOGGING_INTERVAL, + 'log_confusion_matrix': COMET_EVAL_LOG_CONFUSION_MATRIX, + 'log_image_predictions': COMET_EVAL_LOG_IMAGE_PREDICTIONS, + 'max_image_predictions': COMET_MAX_IMAGE_PREDICTIONS, }) + experiment.log_other('Created from', 'yolov8') + except Exception as e: LOGGER.warning(f'WARNING ⚠️ Comet installed but not initialized correctly, not logging this run. {e}') +def _fetch_trainer_metadata(trainer): + curr_epoch = trainer.epoch + 1 + + train_num_steps_per_epoch = len(trainer.train_loader.dataset) // trainer.batch_size + curr_step = curr_epoch * train_num_steps_per_epoch + final_epoch = curr_epoch == trainer.epochs + + save = trainer.args.save + save_period = trainer.args.save_period + save_interval = curr_epoch % save_period == 0 + save_assets = save and save_period > 0 and save_interval and not final_epoch + + return dict(curr_epoch=curr_epoch, curr_step=curr_step, save_assets=save_assets, final_epoch=final_epoch) + + +def _scale_bounding_box_to_original_image_shape(box, resized_image_shape, original_image_shape, ratio_pad): + """YOLOv8 resizes images during training and the label values + are normalized based on this resized shape. This function rescales the + bounding box labels to the original image shape. + """ + + resized_image_height, resized_image_width = resized_image_shape + + # convert normalized xywh format predictions to xyxy in resized scale format + box = ops.xywhn2xyxy(box, h=resized_image_height, w=resized_image_width) + # 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) + # Convert bounding box format from xyxy to xywh for Comet logging + box = ops.xyxy2xywh(box) + # adjust xy center to correspond top-left corner + box[:2] -= box[2:] / 2 + box = box.tolist() + + return box + + +def _format_ground_truth_annotations_for_detection(img_idx, image_path, batch, class_name_map=None): + indices = batch['batch_idx'] == img_idx + bboxes = batch['bboxes'][indices] + if len(bboxes) == 0: + LOGGER.debug(f'COMET WARNING: Image: {image_path} has no bounding boxes labels') + return None + + cls_labels = batch['cls'][indices].squeeze(1).tolist() + if class_name_map: + cls_labels = [str(class_name_map[label]) for label in cls_labels] + + original_image_shape = batch['ori_shape'][img_idx] + resized_image_shape = batch['resized_shape'][img_idx] + ratio_pad = batch['ratio_pad'][img_idx] + + data = [] + for box, label in zip(bboxes, cls_labels): + box = _scale_bounding_box_to_original_image_shape(box, resized_image_shape, original_image_shape, ratio_pad) + data.append({'boxes': [box], 'label': f'gt_{label}', 'score': COMET_MAX_CONFIDENCE_SCORE}) + + return {'name': 'ground_truth', 'data': data} + + +def _format_prediction_annotations_for_detection(image_path, metadata, class_label_map=None): + stem = image_path.stem + image_id = int(stem) if stem.isnumeric() else stem + + predictions = metadata.get(image_id) + if not predictions: + LOGGER.debug(f'COMET WARNING: Image: {image_path} has no bounding boxes predictions') + return None + + data = [] + for prediction in predictions: + boxes = prediction['bbox'] + score = prediction['score'] * COMET_MAX_CONFIDENCE_SCORE + cls_label = prediction['category_id'] + if class_label_map: + cls_label = str(class_label_map[cls_label]) + + data.append({'boxes': [boxes], 'label': cls_label, 'score': score}) + + return {'name': 'prediction', 'data': data} + + +def _fetch_annotations(img_idx, image_path, batch, prediction_metadata_map, class_label_map): + ground_truth_annotations = _format_ground_truth_annotations_for_detection(img_idx, image_path, batch, + class_label_map) + prediction_annotations = _format_prediction_annotations_for_detection(image_path, prediction_metadata_map, + class_label_map) + + annotations = [ + annotation for annotation in [ground_truth_annotations, prediction_annotations] if annotation is not None] + return [annotations] if annotations else None + + +def _create_prediction_metadata_map(model_predictions): + pred_metadata_map = {} + for prediction in model_predictions: + pred_metadata_map.setdefault(prediction['image_id'], []) + pred_metadata_map[prediction['image_id']].append(prediction) + + return pred_metadata_map + + +def _log_confusion_matrix(experiment, trainer, curr_step, curr_epoch): + conf_mat = trainer.validator.confusion_matrix.matrix + names = list(trainer.data['names'].values()) + ['background'] + experiment.log_confusion_matrix( + matrix=conf_mat, + labels=names, + max_categories=len(names), + epoch=curr_epoch, + step=curr_step, + ) + + +def _log_images(experiment, image_paths, curr_step, annotations=None): + if annotations: + for image_path, annotation in zip(image_paths, annotations): + experiment.log_image(image_path, name=image_path.stem, step=curr_step, annotations=annotation) + + else: + for image_path in image_paths: + experiment.log_image(image_path, name=image_path.stem, step=curr_step) + + +def _log_image_predictions(experiment, validator, curr_step): + global _comet_image_prediction_count + + task = validator.args.task + if task not in COMET_SUPPORTED_TASKS: + return + + jdict = validator.jdict + if not jdict: + return + + predictions_metadata_map = _create_prediction_metadata_map(jdict) + dataloader = validator.dataloader + class_label_map = validator.names + + for batch_idx, batch in enumerate(dataloader): + if (batch_idx + 1) % COMET_EVAL_BATCH_LOGGING_INTERVAL != 0: + continue + + image_paths = batch['im_file'] + for img_idx, image_path in enumerate(image_paths): + if _comet_image_prediction_count >= COMET_MAX_IMAGE_PREDICTIONS: + return + + image_path = Path(image_path) + annotations = _fetch_annotations( + img_idx, + image_path, + batch, + predictions_metadata_map, + class_label_map, + ) + _log_images( + experiment, + [image_path], + curr_step, + annotations=annotations, + ) + _comet_image_prediction_count += 1 + + +def _log_plots(experiment, trainer): + plot_filenames = [trainer.save_dir / f'{plots}.png' for plots in EVALUATION_PLOT_NAMES] + _log_images(experiment, plot_filenames, None) + + label_plot_filenames = [trainer.save_dir / f'{labels}.jpg' for labels in LABEL_PLOT_NAMES] + _log_images(experiment, label_plot_filenames, None) + + +def _log_model(experiment, trainer): + experiment.log_model( + COMET_MODEL_NAME, + file_or_folder=str(trainer.best), + file_name='best.pt', + overwrite=True, + ) + + +def on_pretrain_routine_start(trainer): + experiment = comet_ml.get_global_experiment() + if not experiment: + _create_experiment(trainer.args) + + def on_train_epoch_end(trainer): experiment = comet_ml.get_global_experiment() - if experiment: - experiment.log_metrics(trainer.label_loss_items(trainer.tloss, prefix='train'), step=trainer.epoch + 1) - if trainer.epoch == 1: - for f in trainer.save_dir.glob('train_batch*.jpg'): - experiment.log_image(f, name=f.stem, step=trainer.epoch + 1) + if not experiment: + return + + metadata = _fetch_trainer_metadata(trainer) + curr_epoch = metadata['curr_epoch'] + curr_step = metadata['curr_step'] + + experiment.log_metrics( + trainer.label_loss_items(trainer.tloss, prefix='train'), + step=curr_step, + epoch=curr_epoch, + ) + + if curr_epoch == 1: + _log_images(experiment, trainer.save_dir.glob('train_batch*.jpg'), curr_step) def on_fit_epoch_end(trainer): experiment = comet_ml.get_global_experiment() - if experiment: - experiment.log_metrics(trainer.metrics, step=trainer.epoch + 1) - if trainer.epoch == 0: - model_info = { - 'model/parameters': get_num_params(trainer.model), - 'model/GFLOPs': round(get_flops(trainer.model), 3), - 'model/speed(ms)': round(trainer.validator.speed['inference'], 3)} - experiment.log_metrics(model_info, step=trainer.epoch + 1) + if not experiment: + return + + metadata = _fetch_trainer_metadata(trainer) + curr_epoch = metadata['curr_epoch'] + curr_step = metadata['curr_step'] + save_assets = metadata['save_assets'] + + experiment.log_metrics(trainer.metrics, step=curr_step, epoch=curr_epoch) + experiment.log_metrics(trainer.lr, step=curr_step, epoch=curr_epoch) + if curr_epoch == 1: + model_info = { + 'model/parameters': get_num_params(trainer.model), + 'model/GFLOPs': round(get_flops(trainer.model), 3), + 'model/speed(ms)': round(trainer.validator.speed['inference'], 3)} + experiment.log_metrics(model_info, step=curr_step, epoch=curr_epoch) + + if not save_assets: + return + + _log_model(experiment, trainer) + if COMET_EVAL_LOG_CONFUSION_MATRIX: + _log_confusion_matrix(experiment, trainer, curr_step, curr_epoch) + if COMET_EVAL_LOG_IMAGE_PREDICTIONS: + _log_image_predictions(experiment, trainer.validator, curr_step) def on_train_end(trainer): experiment = comet_ml.get_global_experiment() - if experiment: - experiment.log_model('YOLOv8', file_or_folder=str(trainer.best), file_name='best.pt', overwrite=True) + if not experiment: + return + + metadata = _fetch_trainer_metadata(trainer) + curr_epoch = metadata['curr_epoch'] + curr_step = metadata['curr_step'] + plots = trainer.args.plots + + _log_model(experiment, trainer) + if plots: + _log_plots(experiment, trainer) + + _log_confusion_matrix(experiment, trainer, curr_step, curr_epoch) + _log_image_predictions(experiment, trainer.validator, curr_step) + experiment.end() + + global _comet_image_prediction_count + _comet_image_prediction_count = 0 callbacks = { diff --git a/ultralytics/yolo/utils/checks.py b/ultralytics/yolo/utils/checks.py index 3f41fa9..c566daf 100644 --- a/ultralytics/yolo/utils/checks.py +++ b/ultralytics/yolo/utils/checks.py @@ -8,7 +8,6 @@ import platform import re import shutil import subprocess -import urllib from pathlib import Path from typing import Optional @@ -20,8 +19,9 @@ import requests import torch from matplotlib import font_manager -from ultralytics.yolo.utils import (AUTOINSTALL, LOGGER, ONLINE, ROOT, USER_CONFIG_DIR, TryExcept, colorstr, downloads, - emojis, is_colab, is_docker, is_kaggle, is_online, is_pip_package) +from ultralytics.yolo.utils import (AUTOINSTALL, LOGGER, ONLINE, ROOT, USER_CONFIG_DIR, TryExcept, clean_url, colorstr, + downloads, emojis, is_colab, is_docker, is_kaggle, is_online, is_pip_package, + url2file) def is_ascii(s) -> bool: @@ -267,9 +267,9 @@ def check_file(file, suffix='', download=True, hard=True): return file elif download and file.lower().startswith(('https://', 'http://', 'rtsp://', 'rtmp://')): # download url = file # warning: Pathlib turns :// -> :/ - file = Path(urllib.parse.unquote(file).split('?')[0]).name # '%2F' to '/', split https://url.com/file.txt?auth + file = url2file(file) # '%2F' to '/', split https://url.com/file.txt?auth if Path(file).exists(): - LOGGER.info(f'Found {url} locally at {file}') # file already exists + LOGGER.info(f'Found {clean_url(url)} locally at {file}') # file already exists else: downloads.safe_download(url=url, file=file, unzip=False) return file diff --git a/ultralytics/yolo/utils/downloads.py b/ultralytics/yolo/utils/downloads.py index b11a789..8469eeb 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, emojis, is_online +from ultralytics.yolo.utils import LOGGER, checks, clean_url, emojis, is_online, url2file GITHUB_ASSET_NAMES = [f'yolov8{k}{suffix}.pt' for k in 'nsmlx' for suffix in ('', '6', '-cls', '-seg', '-pose')] + \ [f'yolov5{k}u.pt' for k in 'nsmlx'] + \ @@ -43,10 +43,18 @@ def unzip_file(file, path=None, exclude=('.DS_Store', '__MACOSX')): if path is None: path = Path(file).parent # default path with ZipFile(file) as zipObj: - for f in zipObj.namelist(): # list all archived filenames in the zip + for i, f in enumerate(zipObj.namelist()): # list all archived filenames in the zip + # If zip does not expand into a directory create a new directory to expand into + if i == 0: + info = zipObj.getinfo(f) + if info.file_size > 0 or not info.filename.endswith('/'): # element is a file and not a directory + path = Path(path) / Path(file).stem # define new unzip directory + unzip_dir = path + else: + unzip_dir = f if all(x not in f for x in exclude): zipObj.extract(f, path=path) - return zipObj.namelist()[0] # return unzip dir + return unzip_dir # return unzip dir def safe_download(url, @@ -79,8 +87,8 @@ def safe_download(url, f = Path(url) # filename else: # does not exist assert dir or file, 'dir or file required for download' - f = dir / Path(url).name if dir else Path(file) - desc = f'Downloading {url} to {f}' + f = dir / url2file(url) if dir else Path(file) + desc = f'Downloading {clean_url(url)} to {f}' LOGGER.info(f'{desc}...') f.parent.mkdir(parents=True, exist_ok=True) # make directory if missing for i in range(retry + 1): @@ -156,9 +164,9 @@ def attempt_download_asset(file, repo='ultralytics/assets', release='v0.0.0'): name = Path(parse.unquote(str(file))).name # decode '%2F' to '/' etc. if str(file).startswith(('http:/', 'https:/')): # download url = str(file).replace(':/', '://') # Pathlib turns :// -> :/ - file = name.split('?')[0] # parse authentication https://url.com/file.txt?auth... + file = url2file(name) # parse authentication https://url.com/file.txt?auth... if Path(file).is_file(): - LOGGER.info(f'Found {url} locally at {file}') # file already exists + LOGGER.info(f'Found {clean_url(url)} locally at {file}') # file already exists else: safe_download(url=url, file=file, min_bytes=1E5) return file diff --git a/ultralytics/yolo/utils/files.py b/ultralytics/yolo/utils/files.py index 72ebdab..e5c0bed 100644 --- a/ultralytics/yolo/utils/files.py +++ b/ultralytics/yolo/utils/files.py @@ -3,7 +3,6 @@ import contextlib import glob import os -import urllib from datetime import datetime from pathlib import Path @@ -80,12 +79,6 @@ def file_size(path): return 0.0 -def url2file(url): - # Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt - url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/ - return Path(urllib.parse.unquote(url)).name.split('?')[0] # '%2F' to '/', split https://url.com/file.txt?auth - - def get_latest_run(search_dir='.'): # 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) diff --git a/ultralytics/yolo/v8/classify/train.py b/ultralytics/yolo/v8/classify/train.py index ec03d1c..7bbb095 100644 --- a/ultralytics/yolo/v8/classify/train.py +++ b/ultralytics/yolo/v8/classify/train.py @@ -13,11 +13,11 @@ from ultralytics.yolo.utils.torch_utils import is_parallel, strip_optimizer class ClassificationTrainer(BaseTrainer): - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): if overrides is None: overrides = {} overrides['task'] = 'classify' - super().__init__(cfg, overrides) + super().__init__(cfg, overrides, _callbacks) def set_model_attributes(self): self.model.names = self.data['names'] diff --git a/ultralytics/yolo/v8/classify/val.py b/ultralytics/yolo/v8/classify/val.py index f4b503b..aede667 100644 --- a/ultralytics/yolo/v8/classify/val.py +++ b/ultralytics/yolo/v8/classify/val.py @@ -8,8 +8,8 @@ from ultralytics.yolo.utils.metrics import ClassifyMetrics class ClassificationValidator(BaseValidator): - def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None): - super().__init__(dataloader, save_dir, pbar, args) + def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None): + super().__init__(dataloader, save_dir, pbar, args, _callbacks) self.args.task = 'classify' self.metrics = ClassifyMetrics() diff --git a/ultralytics/yolo/v8/detect/val.py b/ultralytics/yolo/v8/detect/val.py index 71044f8..d13274b 100644 --- a/ultralytics/yolo/v8/detect/val.py +++ b/ultralytics/yolo/v8/detect/val.py @@ -18,8 +18,8 @@ from ultralytics.yolo.utils.torch_utils import de_parallel class DetectionValidator(BaseValidator): - def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None): - super().__init__(dataloader, save_dir, pbar, args) + def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None): + super().__init__(dataloader, save_dir, pbar, args, _callbacks) self.args.task = 'detect' self.is_coco = False self.class_map = None diff --git a/ultralytics/yolo/v8/pose/train.py b/ultralytics/yolo/v8/pose/train.py index dd2dbb9..8b928f5 100644 --- a/ultralytics/yolo/v8/pose/train.py +++ b/ultralytics/yolo/v8/pose/train.py @@ -20,11 +20,11 @@ from ultralytics.yolo.v8.detect.train import Loss # BaseTrainer python usage class PoseTrainer(v8.detect.DetectionTrainer): - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): if overrides is None: overrides = {} overrides['task'] = 'pose' - super().__init__(cfg, overrides) + super().__init__(cfg, overrides, _callbacks) def get_model(self, cfg=None, weights=None, verbose=True): model = PoseModel(cfg, ch=3, nc=self.data['nc'], data_kpt_shape=self.data['kpt_shape'], verbose=verbose) diff --git a/ultralytics/yolo/v8/pose/val.py b/ultralytics/yolo/v8/pose/val.py index b849abb..1834e48 100644 --- a/ultralytics/yolo/v8/pose/val.py +++ b/ultralytics/yolo/v8/pose/val.py @@ -14,8 +14,8 @@ from ultralytics.yolo.v8.detect import DetectionValidator class PoseValidator(DetectionValidator): - def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None): - super().__init__(dataloader, save_dir, pbar, args) + def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None): + super().__init__(dataloader, save_dir, pbar, args, _callbacks) self.args.task = 'pose' self.metrics = PoseMetrics(save_dir=self.save_dir) diff --git a/ultralytics/yolo/v8/segment/train.py b/ultralytics/yolo/v8/segment/train.py index 86d7433..165079a 100644 --- a/ultralytics/yolo/v8/segment/train.py +++ b/ultralytics/yolo/v8/segment/train.py @@ -17,11 +17,11 @@ from ultralytics.yolo.v8.detect.train import Loss # BaseTrainer python usage class SegmentationTrainer(v8.detect.DetectionTrainer): - def __init__(self, cfg=DEFAULT_CFG, overrides=None): + def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None): if overrides is None: overrides = {} overrides['task'] = 'segment' - super().__init__(cfg, overrides) + super().__init__(cfg, overrides, _callbacks) def get_model(self, cfg=None, weights=None, verbose=True): model = SegmentationModel(cfg, ch=3, nc=self.data['nc'], verbose=verbose and RANK == -1) diff --git a/ultralytics/yolo/v8/segment/val.py b/ultralytics/yolo/v8/segment/val.py index 52b56e9..2beefcf 100644 --- a/ultralytics/yolo/v8/segment/val.py +++ b/ultralytics/yolo/v8/segment/val.py @@ -16,8 +16,8 @@ from ultralytics.yolo.v8.detect import DetectionValidator class SegmentationValidator(DetectionValidator): - def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None): - super().__init__(dataloader, save_dir, pbar, args) + def __init__(self, dataloader=None, save_dir=None, pbar=None, args=None, _callbacks=None): + super().__init__(dataloader, save_dir, pbar, args, _callbacks) self.args.task = 'segment' self.metrics = SegmentMetrics(save_dir=self.save_dir)