From 1a779ee2b11f48d3ce51ac61f80a6453c8c4e8bc Mon Sep 17 00:00:00 2001
From: Glenn Jocher
+
+
+
+
+
+
安装
-克隆 repo,并要求在 [**Python>=3.7.0**](https://www.python.org/) 环境中安装 [requirements.txt](https://github.com/ultralytics/yolov5/blob/master/requirements.txt) ,且要求 [**PyTorch>=1.7**](https://pytorch.org/get-started/locally/) 。
+克隆 repo,并要求在 [**Python>=3.7.0**](https://www.python.org/) 环境中安装 [requirements.txt](https://github.com/ultralytics/yolov5/blob/master/requirements.txt) ,且要求 [**PyTorch>=1.8**](https://pytorch.org/get-started/locally/) 。
```bash
git clone https://github.com/ultralytics/yolov5 # clone
diff --git a/models/experimental.py b/models/experimental.py
index d60d1808da11..11f75e2254b3 100644
--- a/models/experimental.py
+++ b/models/experimental.py
@@ -87,11 +87,11 @@ def attempt_load(weights, device=None, inplace=True, fuse=True):
model.append(ckpt.fuse().eval() if fuse and hasattr(ckpt, 'fuse') else ckpt.eval()) # model in eval mode
- # Module compatibility updates
+ # Module updates
for m in model.modules():
t = type(m)
if t in (nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model):
- m.inplace = inplace # torch 1.7.0 compatibility
+ m.inplace = inplace
if t is Detect and not isinstance(m.anchor_grid, list):
delattr(m, 'anchor_grid')
setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl)
diff --git a/requirements.txt b/requirements.txt
index ee9e7dbcfb80..33bb7dba2611 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,8 +12,8 @@ PyYAML>=5.3.1
requests>=2.23.0
scipy>=1.4.1
thop>=0.1.1 # FLOPs computation
-torch>=1.7.0 # see https://pytorch.org/get-started/locally (recommended)
-torchvision>=0.8.1
+torch>=1.8.0 # see https://pytorch.org/get-started/locally (recommended)
+torchvision>=0.9.0
tqdm>=4.64.0
ultralytics>=8.0.147
# protobuf<=3.20.1 # https://github.com/ultralytics/yolov5/issues/8012
From 493981cd3b87112fe0e32fe20478b83b3ef33659 Mon Sep 17 00:00:00 2001
From: Glenn Jocher 安装
-克隆 repo,并要求在 [**Python>=3.7.0**](https://www.python.org/) 环境中安装 [requirements.txt](https://github.com/ultralytics/yolov5/blob/master/requirements.txt) ,且要求 [**PyTorch>=1.8**](https://pytorch.org/get-started/locally/) 。
+克隆 repo,并要求在 [**Python>=3.8.0**](https://www.python.org/) 环境中安装 [requirements.txt](https://github.com/ultralytics/yolov5/blob/master/requirements.txt) ,且要求 [**PyTorch>=1.8**](https://pytorch.org/get-started/locally/) 。
```bash
git clone https://github.com/ultralytics/yolov5 # clone
diff --git a/utils/general.py b/utils/general.py
index 017103752ef7..135141e21436 100644
--- a/utils/general.py
+++ b/utils/general.py
@@ -381,7 +381,7 @@ def check_git_info(path='.'):
return {'remote': None, 'branch': None, 'commit': None}
-def check_python(minimum='3.7.0'):
+def check_python(minimum='3.8.0'):
# Check current python version vs. required python version
check_version(platform.python_version(), minimum, name='Python ', hard=True)
From dd104811c2e0419529aa7177ce160321e9694837 Mon Sep 17 00:00:00 2001
From: Glenn Jocher 安装
From 6262c7feb42cd181f165681b9aff428785c0ff7e Mon Sep 17 00:00:00 2001
From: Glenn Jocher
From bb9706e7d179fbf30a5eedd0b0008d1ec470f768 Mon Sep 17 00:00:00 2001
From: Luis Filipe Araujo de Souza <58831491+Doquey@users.noreply.github.com>
Date: Fri, 29 Sep 2023 14:23:05 -0300
Subject: [PATCH 18/79] ONNX export Path to str() (#12177)
* Update export.py
Signed-off-by: Luis Filipe Araujo de Souza <58831491+Doquey@users.noreply.github.com>
* Update export.py
Signed-off-by: Luis Filipe Araujo de Souza <58831491+Doquey@users.noreply.github.com>
* Update export.py
Transformed the f variable into a string on the export onnx. This bug was making it impossible to export any models in .onnx, since it was making the typehint not accept the users input as it is specified in the functions documentation
Signed-off-by: Luis Filipe Araujo de Souza <58831491+Doquey@users.noreply.github.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Signed-off-by: Luis Filipe Araujo de Souza <58831491+Doquey@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
export.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/export.py b/export.py
index 92d42472dfc4..71e4eb94d1c4 100644
--- a/export.py
+++ b/export.py
@@ -155,7 +155,7 @@ def export_onnx(model, im, file, opset, dynamic, simplify, prefix=colorstr('ONNX
import onnx
LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...')
- f = file.with_suffix('.onnx')
+ f = str(file.with_suffix('.onnx'))
output_names = ['output0', 'output1'] if isinstance(model, SegmentationModel) else ['output0']
if dynamic:
From dd9e3382c9af9697fb071d26f1fd1698e9be3e04 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 3 Oct 2023 15:46:59 +0200
Subject: [PATCH 19/79] [pre-commit.ci] pre-commit suggestions (#12189)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/asottile/pyupgrade: v3.10.1 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.14.0)
- [github.com/google/yapf: v0.40.0 → v0.40.2](https://github.com/google/yapf/compare/v0.40.0...v0.40.2)
- [github.com/codespell-project/codespell: v2.2.5 → v2.2.6](https://github.com/codespell-project/codespell/compare/v2.2.5...v2.2.6)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ff2190614378..39ab266f70f3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ repos:
- id: detect-private-key
- repo: https://github.com/asottile/pyupgrade
- rev: v3.10.1
+ rev: v3.14.0
hooks:
- id: pyupgrade
name: Upgrade code
@@ -34,7 +34,7 @@ repos:
name: Sort imports
- repo: https://github.com/google/yapf
- rev: v0.40.0
+ rev: v0.40.2
hooks:
- id: yapf
name: YAPF formatting
@@ -56,7 +56,7 @@ repos:
name: PEP8
- repo: https://github.com/codespell-project/codespell
- rev: v2.2.5
+ rev: v2.2.6
hooks:
- id: codespell
args:
From e4df1ec5bab52601d5de6d62d428dfd03ab53be1 Mon Sep 17 00:00:00 2001
From: Glenn Jocher Inference
-YOLOv5 [PyTorch Hub](https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading) inference. [Models](https://github.com/ultralytics/yolov5/tree/master/models) download automatically from the latest
-YOLOv5 [release](https://github.com/ultralytics/yolov5/releases).
+YOLOv5 [PyTorch Hub](https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading) inference. [Models](https://github.com/ultralytics/yolov5/tree/master/models) download automatically from the latest YOLOv5 [release](https://github.com/ultralytics/yolov5/releases).
```python
import torch
@@ -120,8 +109,7 @@ results.print() # or .show(), .save(), .crop(), .pandas(), etc.
Inference with detect.py
-`detect.py` runs inference on a variety of sources, downloading [models](https://github.com/ultralytics/yolov5/tree/master/models) automatically from
-the latest YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) and saving results to `runs/detect`.
+`detect.py` runs inference on a variety of sources, downloading [models](https://github.com/ultralytics/yolov5/tree/master/models) automatically from the latest YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) and saving results to `runs/detect`.
```bash
python detect.py --weights yolov5s.pt --source 0 # webcam
@@ -143,11 +131,7 @@ python detect.py --weights yolov5s.pt --source 0 #
The commands below reproduce YOLOv5 [COCO](https://github.com/ultralytics/yolov5/blob/master/data/scripts/get_coco.sh)
results. [Models](https://github.com/ultralytics/yolov5/tree/master/models)
-and [datasets](https://github.com/ultralytics/yolov5/tree/master/data) download automatically from the latest
-YOLOv5 [release](https://github.com/ultralytics/yolov5/releases). Training times for YOLOv5n/s/m/l/x are
-1/2/4/6/8 days on a V100 GPU ([Multi-GPU](https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training) times faster). Use the
-largest `--batch-size` possible, or pass `--batch-size -1` for
-YOLOv5 [AutoBatch](https://github.com/ultralytics/yolov5/pull/5092). Batch sizes shown for V100-16GB.
+and [datasets](https://github.com/ultralytics/yolov5/tree/master/data) download automatically from the latest YOLOv5 [release](https://github.com/ultralytics/yolov5/releases). Training times for YOLOv5n/s/m/l/x are 1/2/4/6/8 days on a V100 GPU ([Multi-GPU](https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training) times faster). Use the largest `--batch-size` possible, or pass `--batch-size -1` for YOLOv5 [AutoBatch](https://github.com/ultralytics/yolov5/pull/5092). Batch sizes shown for V100-16GB.
```bash
python train.py --data coco.yaml --epochs 300 --weights '' --cfg yolov5n.yaml --batch-size 128
@@ -476,26 +460,19 @@ For YOLOv5 bug reports and feature requests please visit [GitHub Issues](https:/
[tta]: https://docs.ultralytics.com/yolov5/tutorials/test_time_augmentation
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 9b7c065b9745..d9816c2d98ee 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -28,33 +28,25 @@ YOLOv5 🚀 是世界上最受欢迎的视觉 AI,代表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
## 推理
-使用 YOLOv5 [PyTorch Hub](https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading) 推理。最新 [模型](https://github.com/ultralytics/yolov5/tree/master/models) 将自动的从
-YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载。
+使用 YOLOv5 [PyTorch Hub](https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading) 推理。最新 [模型](https://github.com/ultralytics/yolov5/tree/master/models) 将自动的从 YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载。
```python
import torch
@@ -113,8 +104,7 @@ results.print() # or .show(), .save(), .crop(), .pandas(), etc.
使用 detect.py 推理
-`detect.py` 在各种来源上运行推理, [模型](https://github.com/ultralytics/yolov5/tree/master/models) 自动从
-最新的YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载,并将结果保存到 `runs/detect` 。
+`detect.py` 在各种来源上运行推理, [模型](https://github.com/ultralytics/yolov5/tree/master/models) 自动从 最新的YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载,并将结果保存到 `runs/detect` 。
```bash
python detect.py --weights yolov5s.pt --source 0 # webcam
@@ -134,12 +124,8 @@ python detect.py --weights yolov5s.pt --source 0 #
训练
-下面的命令重现 YOLOv5 在 [COCO](https://github.com/ultralytics/yolov5/blob/master/data/scripts/get_coco.sh) 数据集上的结果。
-最新的 [模型](https://github.com/ultralytics/yolov5/tree/master/models) 和 [数据集](https://github.com/ultralytics/yolov5/tree/master/data)
-将自动的从 YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载。
-YOLOv5n/s/m/l/x 在 V100 GPU 的训练时间为 1/2/4/6/8 天( [多GPU](https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training) 训练速度更快)。
-尽可能使用更大的 `--batch-size` ,或通过 `--batch-size -1` 实现
-YOLOv5 [自动批处理](https://github.com/ultralytics/yolov5/pull/5092) 。下方显示的 batchsize 适用于 V100-16GB。
+下面的命令重现 YOLOv5 在 [COCO](https://github.com/ultralytics/yolov5/blob/master/data/scripts/get_coco.sh) 数据集上的结果。 最新的 [模型](https://github.com/ultralytics/yolov5/tree/master/models) 和 [数据集](https://github.com/ultralytics/yolov5/tree/master/data)
+将自动的从 YOLOv5 [release](https://github.com/ultralytics/yolov5/releases) 中下载。 YOLOv5n/s/m/l/x 在 V100 GPU 的训练时间为 1/2/4/6/8 天( [多GPU](https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training) 训练速度更快)。 尽可能使用更大的 `--batch-size` ,或通过 `--batch-size -1` 实现 YOLOv5 [自动批处理](https://github.com/ultralytics/yolov5/pull/5092) 。下方显示的 batchsize 适用于 V100-16GB。
```bash
python train.py --data coco.yaml --epochs 300 --weights '' --cfg yolov5n.yaml --batch-size 128
@@ -254,7 +240,7 @@ YOLOv5 超级容易上手,简单易学。我们优先考虑现实世界的结
[tta]: https://docs.ultralytics.com/yolov5/tutorials/test_time_augmentation
From c2f131abbeed0f5f852f30d0ac18aa00e1bde1c5 Mon Sep 17 00:00:00 2001
From: Glenn Jocher
- - + + runs/exp{sep}2, runs/exp{sep}3, ... etc. + """ + Generates an incremented file or directory path if it exists, with optional mkdir; args: path, exist_ok=False, + sep="", mkdir=False. + + Example: runs/exp --> runs/exp{sep}2, runs/exp{sep}3, ... etc + """ path = Path(path) # os-agnostic if path.exists() and not exist_ok: path, suffix = (path.with_suffix(""), path.suffix) if path.is_file() else (path, "") @@ -1202,10 +1250,14 @@ def increment_path(path, exist_ok=False, sep="", mkdir=False): def imread(filename, flags=cv2.IMREAD_COLOR): + """Reads an image from a file and returns it as a numpy array, using OpenCV's imdecode to support multilanguage + paths. + """ return cv2.imdecode(np.fromfile(filename, np.uint8), flags) def imwrite(filename, img): + """Writes an image to a file, returns True on success and False on failure, supports multilanguage paths.""" try: cv2.imencode(Path(filename).suffix, img)[1].tofile(filename) return True @@ -1214,6 +1266,7 @@ def imwrite(filename, img): def imshow(path, im): + """Displays an image using Unicode path, requires encoded path and image matrix as input.""" imshow_(path.encode("unicode_escape").decode(), im) diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 36792979913a..c3fbded50a3c 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -73,6 +73,7 @@ def _json_default(value): class Loggers: # YOLOv5 Loggers class def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, include=LOGGERS): + """Initializes loggers for YOLOv5 training and validation metrics, paths, and options.""" self.save_dir = save_dir self.weights = weights self.opt = opt @@ -150,7 +151,7 @@ def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, @property def remote_dataset(self): - # Get data_dict if custom dataset artifact link is provided + """Fetches dataset dictionary from remote logging services like ClearML, Weights & Biases, or Comet ML.""" data_dict = None if self.clearml: data_dict = self.clearml.data_dict @@ -162,15 +163,17 @@ def remote_dataset(self): return data_dict def on_train_start(self): + """Initializes the training process for Comet ML logger if it's configured.""" if self.comet_logger: self.comet_logger.on_train_start() def on_pretrain_routine_start(self): + """Invokes pre-training routine start hook for Comet ML logger if available.""" if self.comet_logger: self.comet_logger.on_pretrain_routine_start() def on_pretrain_routine_end(self, labels, names): - # Callback runs on pre-train routine end + """Callback that runs at the end of pre-training routine, logging label plots if enabled.""" if self.plots: plot_labels(labels, names, self.save_dir) paths = self.save_dir.glob("*labels*.jpg") # training labels @@ -183,6 +186,7 @@ def on_pretrain_routine_end(self, labels, names): self.clearml.log_plot(title=path.stem, plot_path=path) def on_train_batch_end(self, model, ni, imgs, targets, paths, vals): + """Logs training batch end events, plots images, and updates external loggers with batch-end data.""" log_dict = dict(zip(self.keys[:3], vals)) # Callback runs on train batch end # ni: number integrated batches (since train start) @@ -203,7 +207,7 @@ def on_train_batch_end(self, model, ni, imgs, targets, paths, vals): self.comet_logger.on_train_batch_end(log_dict, step=ni) def on_train_epoch_end(self, epoch): - # Callback runs on train epoch end + """Callback that updates the current epoch in Weights & Biases at the end of a training epoch.""" if self.wandb: self.wandb.current_epoch = epoch + 1 @@ -211,22 +215,24 @@ def on_train_epoch_end(self, epoch): self.comet_logger.on_train_epoch_end(epoch) def on_val_start(self): + """Callback that signals the start of a validation phase to the Comet logger.""" if self.comet_logger: self.comet_logger.on_val_start() def on_val_image_end(self, pred, predn, path, names, im): - # Callback runs on val image end + """Callback that logs a validation image and its predictions to WandB or ClearML.""" if self.wandb: self.wandb.val_one_image(pred, predn, path, names, im) if self.clearml: self.clearml.log_image_with_boxes(path, pred, names, im) def on_val_batch_end(self, batch_i, im, targets, paths, shapes, out): + """Logs validation batch results to Comet ML during training at the end of each validation batch.""" if self.comet_logger: self.comet_logger.on_val_batch_end(batch_i, im, targets, paths, shapes, out) def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix): - # Callback runs on val end + """Logs validation results to WandB or ClearML at the end of the validation process.""" if self.wandb or self.clearml: files = sorted(self.save_dir.glob("val*.jpg")) if self.wandb: @@ -238,7 +244,7 @@ def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix) self.comet_logger.on_val_end(nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix) def on_fit_epoch_end(self, vals, epoch, best_fitness, fi): - # Callback runs at the end of each fit (train+val) epoch + """Callback that logs metrics and saves them to CSV or NDJSON at the end of each fit (train+val) epoch.""" x = dict(zip(self.keys, vals)) if self.csv: file = self.save_dir / "results.csv" @@ -277,7 +283,7 @@ def on_fit_epoch_end(self, vals, epoch, best_fitness, fi): self.comet_logger.on_fit_epoch_end(x, epoch=epoch) def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): - # Callback runs on model save event + """Callback that handles model saving events, logging to Weights & Biases or ClearML if enabled.""" if (epoch + 1) % self.opt.save_period == 0 and not final_epoch and self.opt.save_period != -1: if self.wandb: self.wandb.log_model(last.parent, self.opt, epoch, fi, best_model=best_fitness == fi) @@ -290,7 +296,7 @@ def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): self.comet_logger.on_model_save(last, epoch, final_epoch, best_fitness, fi) def on_train_end(self, last, best, epoch, results): - # Callback runs on training end, i.e. saving best model + """Callback that runs at the end of training to save plots and log results.""" if self.plots: plot_results(file=self.save_dir / "results.csv") # save results.png files = ["results.png", "confusion_matrix.png", *(f"{x}_curve.png" for x in ("F1", "PR", "P", "R"))] @@ -326,7 +332,7 @@ def on_train_end(self, last, best, epoch, results): self.comet_logger.on_train_end(files, self.save_dir, last, best, epoch, final_results) def on_params_update(self, params: dict): - # Update hyperparams or configs of the experiment + """Updates experiment hyperparameters or configurations in WandB, Comet, or ClearML.""" if self.wandb: self.wandb.wandb_run.config.update(params, allow_val_change=True) if self.comet_logger: @@ -346,7 +352,7 @@ class GenericLogger: """ def __init__(self, opt, console_logger, include=("tb", "wandb", "clearml")): - # init default loggers + """Initializes a generic logger with optional TensorBoard, W&B, and ClearML support.""" self.save_dir = Path(opt.save_dir) self.include = include self.console_logger = console_logger @@ -381,7 +387,7 @@ def __init__(self, opt, console_logger, include=("tb", "wandb", "clearml")): self.clearml = None def log_metrics(self, metrics, epoch): - # Log metrics dictionary to all loggers + """Logs metrics to CSV, TensorBoard, W&B, and ClearML; `metrics` is a dict, `epoch` is an int.""" if self.csv: keys, vals = list(metrics.keys()), list(metrics.values()) n = len(metrics) + 1 # number of cols @@ -400,7 +406,7 @@ def log_metrics(self, metrics, epoch): self.clearml.log_scalars(metrics, epoch) def log_images(self, files, name="Images", epoch=0): - # Log images to all loggers + """Logs images to all loggers with optional naming and epoch specification.""" files = [Path(f) for f in (files if isinstance(files, (tuple, list)) else [files])] # to Path files = [f for f in files if f.exists()] # filter by exists @@ -418,11 +424,12 @@ def log_images(self, files, name="Images", epoch=0): self.clearml.log_debug_samples(files, title=name) def log_graph(self, model, imgsz=(640, 640)): - # Log model graph to all loggers + """Logs model graph to all configured loggers with specified input image size.""" if self.tb: log_tensorboard_graph(self.tb, model, imgsz) def log_model(self, model_path, epoch=0, metadata=None): + """Logs the model to all configured loggers with optional epoch and metadata.""" if metadata is None: metadata = {} # Log model to all loggers @@ -434,7 +441,7 @@ def log_model(self, model_path, epoch=0, metadata=None): self.clearml.log_model(model_path=model_path, model_name=model_path.stem) def update_params(self, params): - # Update the parameters logged + """Updates logged parameters in WandB and/or ClearML if enabled.""" if self.wandb: wandb.run.config.update(params, allow_val_change=True) if self.clearml: @@ -442,7 +449,7 @@ def update_params(self, params): def log_tensorboard_graph(tb, model, imgsz=(640, 640)): - # Log model graph to TensorBoard + """Logs the model graph to TensorBoard with specified image size and model.""" try: p = next(model.parameters()) # for device, type imgsz = (imgsz, imgsz) if isinstance(imgsz, int) else imgsz # expand @@ -455,7 +462,7 @@ def log_tensorboard_graph(tb, model, imgsz=(640, 640)): def web_project_name(project): - # Convert local project name to web project name + """Converts a local project name to a standardized web project name with optional suffixes.""" if not project.startswith("runs/train"): return project suffix = "-Classify" if project.endswith("-cls") else "-Segment" if project.endswith("-seg") else "" diff --git a/utils/loggers/comet/__init__.py b/utils/loggers/comet/__init__.py index cec46f5af1fb..076eb3ccecab 100644 --- a/utils/loggers/comet/__init__.py +++ b/utils/loggers/comet/__init__.py @@ -165,6 +165,7 @@ def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwar self.experiment.log_other("optimizer_parameters", json.dumps(self.hyp)) def _get_experiment(self, mode, experiment_id=None): + """Returns a new or existing Comet.ml experiment based on mode and optional experiment_id.""" if mode == "offline": return ( comet_ml.ExistingOfflineExperiment( @@ -197,21 +198,27 @@ def _get_experiment(self, mode, experiment_id=None): return def log_metrics(self, log_dict, **kwargs): + """Logs metrics to the current experiment, accepting a dictionary of metric names and values.""" self.experiment.log_metrics(log_dict, **kwargs) def log_parameters(self, log_dict, **kwargs): + """Logs parameters to the current experiment, accepting a dictionary of parameter names and values.""" self.experiment.log_parameters(log_dict, **kwargs) def log_asset(self, asset_path, **kwargs): + """Logs a file or directory as an asset to the current experiment.""" self.experiment.log_asset(asset_path, **kwargs) def log_asset_data(self, asset, **kwargs): + """Logs in-memory data as an asset to the current experiment, with optional kwargs.""" self.experiment.log_asset_data(asset, **kwargs) def log_image(self, img, **kwargs): + """Logs an image to the current experiment with optional kwargs.""" self.experiment.log_image(img, **kwargs) def log_model(self, path, opt, epoch, fitness_score, best_model=False): + """Logs model checkpoint to experiment with path, options, epoch, fitness, and best model flag.""" if not self.save_model: return @@ -235,6 +242,7 @@ def log_model(self, path, opt, epoch, fitness_score, best_model=False): ) def check_dataset(self, data_file): + """Validates the dataset configuration by loading the YAML file specified in `data_file`.""" with open(data_file) as f: data_config = yaml.safe_load(f) @@ -247,6 +255,7 @@ def check_dataset(self, data_file): return check_dataset(data_file) def log_predictions(self, image, labelsn, path, shape, predn): + """Logs predictions with IOU filtering, given image, labels, path, shape, and predictions.""" if self.logged_images_count >= self.max_images: return detections = predn[predn[:, 4] > self.conf_thres] @@ -287,6 +296,7 @@ def log_predictions(self, image, labelsn, path, shape, predn): return def preprocess_prediction(self, image, labels, shape, pred): + """Processes prediction data, resizing labels and adding dataset metadata.""" nl, _ = labels.shape[0], pred.shape[0] # Predictions @@ -306,6 +316,7 @@ def preprocess_prediction(self, image, labels, shape, pred): return predn, labelsn def add_assets_to_artifact(self, artifact, path, asset_path, split): + """Adds image and label assets to a wandb artifact given dataset split and paths.""" img_paths = sorted(glob.glob(f"{asset_path}/*")) label_paths = img2label_paths(img_paths) @@ -331,6 +342,7 @@ def add_assets_to_artifact(self, artifact, path, asset_path, split): return artifact def upload_dataset_artifact(self): + """Uploads a YOLOv5 dataset as an artifact to the Comet.ml platform.""" dataset_name = self.data_dict.get("dataset_name", "yolov5-dataset") path = str((ROOT / Path(self.data_dict["path"])).resolve()) @@ -355,6 +367,7 @@ def upload_dataset_artifact(self): return def download_dataset_artifact(self, artifact_path): + """Downloads a dataset artifact to a specified directory using the experiment's logged artifact.""" logged_artifact = self.experiment.get_artifact(artifact_path) artifact_save_dir = str(Path(self.opt.save_dir) / logged_artifact.name) logged_artifact.download(artifact_save_dir) @@ -374,6 +387,7 @@ def download_dataset_artifact(self, artifact_path): return self.update_data_paths(data_dict) def update_data_paths(self, data_dict): + """Updates data paths in the dataset dictionary, defaulting 'path' to an empty string if not present.""" path = data_dict.get("path", "") for split in ["train", "val", "test"]: @@ -386,6 +400,7 @@ def update_data_paths(self, data_dict): return data_dict def on_pretrain_routine_end(self, paths): + """Called at the end of pretraining routine to handle paths if training is not being resumed.""" if self.opt.resume: return @@ -398,20 +413,25 @@ def on_pretrain_routine_end(self, paths): return def on_train_start(self): + """Logs hyperparameters at the start of training.""" self.log_parameters(self.hyp) def on_train_epoch_start(self): + """Called at the start of each training epoch.""" return def on_train_epoch_end(self, epoch): + """Updates the current epoch in the experiment tracking at the end of each epoch.""" self.experiment.curr_epoch = epoch return def on_train_batch_start(self): + """Called at the start of each training batch.""" return def on_train_batch_end(self, log_dict, step): + """Callback function that updates and logs metrics at the end of each training batch if conditions are met.""" self.experiment.curr_step = step if self.log_batch_metrics and (step % self.comet_log_batch_interval == 0): self.log_metrics(log_dict, step=step) @@ -419,6 +439,7 @@ def on_train_batch_end(self, log_dict, step): return def on_train_end(self, files, save_dir, last, best, epoch, results): + """Logs metadata and optionally saves model files at the end of training.""" if self.comet_log_predictions: curr_epoch = self.experiment.curr_epoch self.experiment.log_asset_data(self.metadata_dict, "image-metadata.json", epoch=curr_epoch) @@ -446,12 +467,15 @@ def on_train_end(self, files, save_dir, last, best, epoch, results): self.finish_run() def on_val_start(self): + """Called at the start of validation, currently a placeholder with no functionality.""" return def on_val_batch_start(self): + """Placeholder called at the start of a validation batch with no current functionality.""" return def on_val_batch_end(self, batch_i, images, targets, paths, shapes, outputs): + """Callback executed at the end of a validation batch, conditionally logs predictions to Comet ML.""" if not (self.comet_log_predictions and ((batch_i + 1) % self.comet_log_prediction_interval == 0)): return @@ -470,6 +494,7 @@ def on_val_batch_end(self, batch_i, images, targets, paths, shapes, outputs): return def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix): + """Logs per-class metrics to Comet.ml after validation if enabled and more than one class exists.""" if self.comet_log_per_class_metrics and self.num_classes > 1: for i, c in enumerate(ap_class): class_name = self.class_names[c] @@ -504,14 +529,18 @@ def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix) ) def on_fit_epoch_end(self, result, epoch): + """Logs metrics at the end of each training epoch.""" self.log_metrics(result, epoch=epoch) def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): + """Callback to save model checkpoints periodically if conditions are met.""" if ((epoch + 1) % self.opt.save_period == 0 and not final_epoch) and self.opt.save_period != -1: self.log_model(last.parent, self.opt, epoch, fi, best_model=best_fitness == fi) def on_params_update(self, params): + """Logs updated parameters during training.""" self.log_parameters(params) def finish_run(self): + """Ends the current experiment and logs its completion.""" self.experiment.end() diff --git a/utils/loggers/comet/comet_utils.py b/utils/loggers/comet/comet_utils.py index 6e8fad68c6cc..7eca1f504d69 100644 --- a/utils/loggers/comet/comet_utils.py +++ b/utils/loggers/comet/comet_utils.py @@ -17,6 +17,7 @@ def download_model_checkpoint(opt, experiment): + """Downloads YOLOv5 model checkpoint from Comet ML experiment, updating `opt.weights` with download path.""" model_dir = f"{opt.project}/{experiment.name}" os.makedirs(model_dir, exist_ok=True) diff --git a/utils/loggers/comet/hpo.py b/utils/loggers/comet/hpo.py index a9e6fabec1cd..8ca08ddc858a 100644 --- a/utils/loggers/comet/hpo.py +++ b/utils/loggers/comet/hpo.py @@ -25,6 +25,9 @@ def get_args(known=False): + """Parses command-line arguments for YOLOv5 training, supporting configuration of weights, data paths, + hyperparameters, and more. + """ parser = argparse.ArgumentParser() parser.add_argument("--weights", type=str, default=ROOT / "yolov5s.pt", help="initial weights path") parser.add_argument("--cfg", type=str, default="", help="model.yaml path") @@ -83,6 +86,7 @@ def get_args(known=False): def run(parameters, opt): + """Executes YOLOv5 training with given hyperparameters and options, setting up device and training directories.""" hyp_dict = {k: v for k, v in parameters.items() if k not in ["epochs", "batch_size"]} opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve)) diff --git a/utils/loggers/wandb/wandb_utils.py b/utils/loggers/wandb/wandb_utils.py index 0af8bda12d85..4083312e6a59 100644 --- a/utils/loggers/wandb/wandb_utils.py +++ b/utils/loggers/wandb/wandb_utils.py @@ -152,6 +152,7 @@ def log_model(self, path, opt, epoch, fitness_score, best_model=False): LOGGER.info(f"Saving model artifact on epoch {epoch + 1}") def val_one_image(self, pred, predn, path, names, im): + """Evaluates model prediction for a single image, returning metrics and visualizations.""" pass def log(self, log_dict): diff --git a/utils/loss.py b/utils/loss.py index 26b8c06bf333..8a910e12ad6f 100644 --- a/utils/loss.py +++ b/utils/loss.py @@ -16,11 +16,17 @@ def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#iss class BCEBlurWithLogitsLoss(nn.Module): # BCEwithLogitLoss() with reduced missing label effects. def __init__(self, alpha=0.05): + """Initializes a modified BCEWithLogitsLoss with reduced missing label effects, taking optional alpha smoothing + parameter. + """ super().__init__() self.loss_fcn = nn.BCEWithLogitsLoss(reduction="none") # must be nn.BCEWithLogitsLoss() self.alpha = alpha def forward(self, pred, true): + """Computes modified BCE loss for YOLOv5 with reduced missing label effects, taking pred and true tensors, + returns mean loss. + """ loss = self.loss_fcn(pred, true) pred = torch.sigmoid(pred) # prob from logits dx = pred - true # reduce only missing label effects @@ -33,6 +39,9 @@ def forward(self, pred, true): class FocalLoss(nn.Module): # Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): + """Initializes FocalLoss with specified loss function, gamma, and alpha values; modifies loss reduction to + 'none'. + """ super().__init__() self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss() self.gamma = gamma @@ -41,6 +50,7 @@ def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): self.loss_fcn.reduction = "none" # required to apply FL to each element def forward(self, pred, true): + """Calculates the focal loss between predicted and true labels using a modified BCEWithLogitsLoss.""" loss = self.loss_fcn(pred, true) # p_t = torch.exp(-loss) # loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability @@ -63,6 +73,7 @@ def forward(self, pred, true): class QFocalLoss(nn.Module): # Wraps Quality focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): + """Initializes Quality Focal Loss with given loss function, gamma, alpha; modifies reduction to 'none'.""" super().__init__() self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss() self.gamma = gamma @@ -71,6 +82,9 @@ def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): self.loss_fcn.reduction = "none" # required to apply FL to each element def forward(self, pred, true): + """Computes the focal loss between `pred` and `true` using BCEWithLogitsLoss, adjusting for imbalance with + `gamma` and `alpha`. + """ loss = self.loss_fcn(pred, true) pred_prob = torch.sigmoid(pred) # prob from logits @@ -91,6 +105,7 @@ class ComputeLoss: # Compute losses def __init__(self, model, autobalance=False): + """Initializes ComputeLoss with model and autobalance option, autobalances losses if True.""" device = next(model.parameters()).device # get model device h = model.hyp # hyperparameters @@ -173,7 +188,9 @@ def __call__(self, p, targets): # predictions, targets return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach() def build_targets(self, p, targets): - # Build targets for compute_loss(), input targets(image,class,x,y,w,h) + """Prepares model targets from input targets (image,class,x,y,w,h) for loss computation, returning class, box, + indices, and anchors. + """ na, nt = self.na, targets.shape[0] # number of anchors, targets tcls, tbox, indices, anch = [], [], [], [] gain = torch.ones(7, device=self.device) # normalized to gridspace gain diff --git a/utils/metrics.py b/utils/metrics.py index 5f45621dc372..e572355fec1e 100644 --- a/utils/metrics.py +++ b/utils/metrics.py @@ -13,13 +13,13 @@ def fitness(x): - # Model fitness as a weighted combination of metrics + """Calculates fitness of a model using weighted sum of metrics P, R, mAP@0.5, mAP@0.5:0.95.""" w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, mAP@0.5, mAP@0.5:0.95] return (x[:, :4] * w).sum(1) def smooth(y, f=0.05): - # Box filter of fraction f + """Applies box filter smoothing to array `y` with fraction `f`, yielding a smoothed array.""" nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd) p = np.ones(nf // 2) # ones padding yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded @@ -126,6 +126,7 @@ def compute_ap(recall, precision): class ConfusionMatrix: # Updated version of https://github.com/kaanakan/object_detection_confusion_matrix def __init__(self, nc, conf=0.25, iou_thres=0.45): + """Initializes ConfusionMatrix with given number of classes, confidence, and IoU threshold.""" self.matrix = np.zeros((nc + 1, nc + 1)) self.nc = nc # number of classes self.conf = conf @@ -179,6 +180,9 @@ def process_batch(self, detections, labels): self.matrix[dc, self.nc] += 1 # predicted background def tp_fp(self): + """Calculates true positives (tp) and false positives (fp) excluding the background class from the confusion + matrix. + """ tp = self.matrix.diagonal() # true positives fp = self.matrix.sum(1) - tp # false positives # fn = self.matrix.sum(0) - tp # false negatives (missed detections) @@ -186,6 +190,7 @@ def tp_fp(self): @TryExcept("WARNING ⚠️ ConfusionMatrix plot failure") def plot(self, normalize=True, save_dir="", names=()): + """Plots confusion matrix using seaborn, optional normalization; can save plot to specified directory.""" import seaborn as sn array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1e-9) if normalize else 1) # normalize columns @@ -217,12 +222,17 @@ def plot(self, normalize=True, save_dir="", names=()): plt.close(fig) def print(self): + """Prints the confusion matrix row-wise, with each class and its predictions separated by spaces.""" for i in range(self.nc + 1): print(" ".join(map(str, self.matrix[i]))) def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7): - # Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4) + """ + Calculates IoU, GIoU, DIoU, or CIoU between two boxes, supporting xywh/xyxy formats. + + Input shapes are box1(1,4) to box2(n,4). + """ # Get the coordinates of bounding boxes if xywh: # transform from xywh to xyxy @@ -312,7 +322,9 @@ def bbox_ioa(box1, box2, eps=1e-7): def wh_iou(wh1, wh2, eps=1e-7): - # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2 + """Calculates the Intersection over Union (IoU) for two sets of widths and heights; `wh1` and `wh2` should be nx2 + and mx2 tensors. + """ wh1 = wh1[:, None] # [N,1,2] wh2 = wh2[None] # [1,M,2] inter = torch.min(wh1, wh2).prod(2) # [N,M] @@ -324,7 +336,9 @@ def wh_iou(wh1, wh2, eps=1e-7): @threaded def plot_pr_curve(px, py, ap, save_dir=Path("pr_curve.png"), names=()): - # Precision-recall curve + """Plots precision-recall curve, optionally per class, saving to `save_dir`; `px`, `py` are lists, `ap` is Nx2 + array, `names` optional. + """ fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) py = np.stack(py, axis=1) @@ -347,7 +361,7 @@ def plot_pr_curve(px, py, ap, save_dir=Path("pr_curve.png"), names=()): @threaded def plot_mc_curve(px, py, save_dir=Path("mc_curve.png"), names=(), xlabel="Confidence", ylabel="Metric"): - # Metric-confidence curve + """Plots a metric-confidence curve for model predictions, supporting per-class visualization and smoothing.""" fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) if 0 < len(names) < 21: # display per-class legend if < 21 classes diff --git a/utils/plots.py b/utils/plots.py index 11c96a6372c3..e1b073dfb1ad 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -31,7 +31,11 @@ class Colors: # Ultralytics color palette https://ultralytics.com/ def __init__(self): - # hex = matplotlib.colors.TABLEAU_COLORS.values() + """ + Initializes the Colors class with a palette derived from Ultralytics color scheme, converting hex codes to RGB. + + Colors derived from `hex = matplotlib.colors.TABLEAU_COLORS.values()`. + """ hexs = ( "FF3838", "FF9D97", @@ -58,6 +62,7 @@ def __init__(self): self.n = len(self.palette) def __call__(self, i, bgr=False): + """Returns color from palette by index `i`, in BGR format if `bgr=True`, else RGB; `i` is an integer index.""" c = self.palette[int(i) % self.n] return (c[2], c[1], c[0]) if bgr else c @@ -100,7 +105,11 @@ def feature_visualization(x, module_type, stage, n=32, save_dir=Path("runs/detec def hist2d(x, y, n=100): - # 2d histogram used in labels.png and evolve.png + """ + Generates a logarithmic 2D histogram, useful for visualizing label or evolution distributions. + + Used in used in labels.png and evolve.png. + """ xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n) hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges)) xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1) @@ -109,6 +118,7 @@ def hist2d(x, y, n=100): def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5): + """Applies a low-pass Butterworth filter to `data` with specified `cutoff`, `fs`, and `order`.""" from scipy.signal import butter, filtfilt # https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy @@ -122,7 +132,9 @@ def butter_lowpass(cutoff, fs, order): def output_to_target(output, max_det=300): - # Convert model output to target format [batch_id, class_id, x, y, w, h, conf] for plotting + """Converts YOLOv5 model output to [batch_id, class_id, x, y, w, h, conf] format for plotting, limiting detections + to `max_det`. + """ targets = [] for i, o in enumerate(output): box, conf, cls = o[:max_det, :6].cpu().split((4, 1, 1), 1) @@ -133,7 +145,7 @@ def output_to_target(output, max_det=300): @threaded def plot_images(images, targets, paths=None, fname="images.jpg", names=None): - # Plot image grid with labels + """Plots an image grid with labels from YOLOv5 predictions or targets, saving to `fname`.""" if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() if isinstance(targets, torch.Tensor): @@ -197,7 +209,7 @@ def plot_images(images, targets, paths=None, fname="images.jpg", names=None): def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=""): - # Plot LR simulating training for full epochs + """Plots learning rate schedule for given optimizer and scheduler, saving plot to `save_dir`.""" optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals y = [] for _ in range(epochs): @@ -295,7 +307,7 @@ def plot_val_study(file="", dir="", x=None): # from utils.plots import *; plot_ @TryExcept() # known issue https://github.com/ultralytics/yolov5/issues/5395 def plot_labels(labels, names=(), save_dir=Path("")): - # plot dataset labels + """Plots dataset labels, saving correlogram and label images, handles classes, and visualizes bounding boxes.""" LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ") c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes nc = int(c.max() + 1) # number of classes @@ -340,7 +352,7 @@ def plot_labels(labels, names=(), save_dir=Path("")): def imshow_cls(im, labels=None, pred=None, names=None, nmax=25, verbose=False, f=Path("images.jpg")): - # Show classification image grid with labels (optional) and predictions (optional) + """Displays a grid of images with optional labels and predictions, saving to a file.""" from utils.augmentations import denormalize names = names or [f"class{i}" for i in range(1000)] @@ -397,7 +409,11 @@ def plot_evolve(evolve_csv="path/to/evolve.csv"): # from utils.plots import *; def plot_results(file="path/to/results.csv", dir=""): - # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') + """ + Plots training results from a 'results.csv' file; accepts file path and directory as arguments. + + Example: from utils.plots import *; plot_results('path/to/results.csv') + """ save_dir = Path(file).parent if file else Path(dir) fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) ax = ax.ravel() @@ -424,7 +440,11 @@ def plot_results(file="path/to/results.csv", dir=""): def profile_idetection(start=0, stop=0, labels=(), save_dir=""): - # Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection() + """ + Plots per-image iDetection logs, comparing metrics like storage and performance over time. + + Example: from utils.plots import *; profile_idetection() + """ ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel() s = ["Images", "Free Storage (GB)", "RAM Usage (GB)", "Battery", "dt_raw (ms)", "dt_smooth (ms)", "real-world FPS"] files = list(Path(save_dir).glob("frames*.txt")) @@ -455,7 +475,9 @@ def profile_idetection(start=0, stop=0, labels=(), save_dir=""): def save_one_box(xyxy, im, file=Path("im.jpg"), gain=1.02, pad=10, square=False, BGR=False, save=True): - # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop + """Crops and saves an image from bounding box `xyxy`, applied with `gain` and `pad`, optionally squares and adjusts + for BGR. + """ xyxy = torch.tensor(xyxy).view(-1, 4) b = xyxy2xywh(xyxy) # boxes if square: diff --git a/utils/segment/augmentations.py b/utils/segment/augmentations.py index 56636b65d93a..e13a53d34821 100644 --- a/utils/segment/augmentations.py +++ b/utils/segment/augmentations.py @@ -12,7 +12,11 @@ def mixup(im, labels, segments, im2, labels2, segments2): - # Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf + """ + Applies MixUp augmentation blending two images, labels, and segments with a random ratio. + + See https://arxiv.org/pdf/1710.09412.pdf + """ r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0 im = (im * r + im2 * (1 - r)).astype(np.uint8) labels = np.concatenate((labels, labels2), 0) diff --git a/utils/segment/dataloaders.py b/utils/segment/dataloaders.py index b0b3a7424216..9d2e9bef0b09 100644 --- a/utils/segment/dataloaders.py +++ b/utils/segment/dataloaders.py @@ -123,6 +123,7 @@ def __init__( self.overlap = overlap def __getitem__(self, index): + """Returns a transformed item from the dataset at the specified index, handling indexing and image weighting.""" index = self.indices[index] # linear, shuffled, or image_weights hyp = self.hyp @@ -230,7 +231,7 @@ def __getitem__(self, index): return (torch.from_numpy(img), labels_out, self.im_files[index], shapes, masks) def load_mosaic(self, index): - # YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic + """Loads 1 image + 3 random images into a 4-image YOLOv5 mosaic, adjusting labels and segments accordingly.""" labels4, segments4 = [], [] s = self.img_size yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y @@ -291,6 +292,7 @@ def load_mosaic(self, index): @staticmethod def collate_fn(batch): + """Custom collation function for DataLoader, batches images, labels, paths, shapes, and segmentation masks.""" img, label, path, shapes, masks = zip(*batch) # transposed batched_masks = torch.cat(masks, 0) for i, l in enumerate(label): diff --git a/utils/segment/general.py b/utils/segment/general.py index 8cbc745b4a90..f292496c0da9 100644 --- a/utils/segment/general.py +++ b/utils/segment/general.py @@ -144,7 +144,9 @@ def masks_iou(mask1, mask2, eps=1e-7): def masks2segments(masks, strategy="largest"): - # Convert masks(n,160,160) into segments(n,xy) + """Converts binary (n,160,160) masks to polygon segments with options for concatenation or selecting the largest + segment. + """ segments = [] for x in masks.int().cpu().numpy().astype("uint8"): c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] diff --git a/utils/segment/loss.py b/utils/segment/loss.py index 1e007271fa9c..29f1bcbb7e77 100644 --- a/utils/segment/loss.py +++ b/utils/segment/loss.py @@ -12,6 +12,9 @@ class ComputeLoss: # Compute losses def __init__(self, model, autobalance=False, overlap=False): + """Initializes the compute loss function for YOLOv5 models with options for autobalancing and overlap + handling. + """ self.sort_obj_iou = False self.overlap = overlap device = next(model.parameters()).device # get model device @@ -109,13 +112,15 @@ def __call__(self, preds, targets, masks): # predictions, targets, model return loss * bs, torch.cat((lbox, lseg, lobj, lcls)).detach() def single_mask_loss(self, gt_mask, pred, proto, xyxy, area): - # Mask loss for one image + """Calculates and normalizes single mask loss for YOLOv5 between predicted and ground truth masks.""" pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n,32) @ (32,80,80) -> (n,80,80) loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction="none") return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean() def build_targets(self, p, targets): - # Build targets for compute_loss(), input targets(image,class,x,y,w,h) + """Prepares YOLOv5 targets for loss computation; inputs targets (image, class, x, y, w, h), output target + classes/boxes. + """ na, nt = self.na, targets.shape[0] # number of anchors, targets tcls, tbox, indices, anch, tidxs, xywhn = [], [], [], [], [], [] gain = torch.ones(8, device=self.device) # normalized to gridspace gain diff --git a/utils/segment/metrics.py b/utils/segment/metrics.py index 7811e7eb364a..973b398eb6b9 100644 --- a/utils/segment/metrics.py +++ b/utils/segment/metrics.py @@ -7,7 +7,7 @@ def fitness(x): - # Model fitness as a weighted combination of metrics + """Evaluates model fitness by a weighted sum of 8 metrics, `x`: [N,8] array, weights: [0.1, 0.9] for mAP and F1.""" w = [0.0, 0.0, 0.1, 0.9, 0.0, 0.0, 0.1, 0.9] return (x[:, :8] * w).sum(1) @@ -128,6 +128,7 @@ def class_result(self, i): return (self.p[i], self.r[i], self.ap50[i], self.ap[i]) def get_maps(self, nc): + """Calculates and returns mean Average Precision (mAP) for each class given number of classes `nc`.""" maps = np.zeros(nc) + self.map for i, c in enumerate(self.ap_class_index): maps[c] = self.ap[i] @@ -162,17 +163,22 @@ def update(self, results): self.metric_mask.update(list(results["masks"].values())) def mean_results(self): + """Computes and returns the mean results for both box and mask metrics by summing their individual means.""" return self.metric_box.mean_results() + self.metric_mask.mean_results() def class_result(self, i): + """Returns the sum of box and mask metric results for a specified class index `i`.""" return self.metric_box.class_result(i) + self.metric_mask.class_result(i) def get_maps(self, nc): + """Calculates and returns the sum of mean average precisions (mAPs) for both box and mask metrics for `nc` + classes. + """ return self.metric_box.get_maps(nc) + self.metric_mask.get_maps(nc) @property def ap_class_index(self): - # boxes and masks have the same ap_class_index + """Returns the class index for average precision, shared by both box and mask metrics.""" return self.metric_box.ap_class_index diff --git a/utils/segment/plots.py b/utils/segment/plots.py index 0e30c61be66f..ce01988be937 100644 --- a/utils/segment/plots.py +++ b/utils/segment/plots.py @@ -15,7 +15,7 @@ @threaded def plot_images_and_masks(images, targets, masks, paths=None, fname="images.jpg", names=None): - # Plot image grid with labels + """Plots a grid of images, their labels, and masks with optional resizing and annotations, saving to fname.""" if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() if isinstance(targets, torch.Tensor): @@ -111,7 +111,11 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname="images.jpg" def plot_results_with_masks(file="path/to/results.csv", dir="", best=True): - # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') + """ + Plots training results from CSV files, plotting best or last result highlights based on `best` parameter. + + Example: from utils.plots import *; plot_results('path/to/results.csv') + """ save_dir = Path(file).parent if file else Path(dir) fig, ax = plt.subplots(2, 8, figsize=(18, 6), tight_layout=True) ax = ax.ravel() diff --git a/utils/torch_utils.py b/utils/torch_utils.py index 6bc4b4c7fd04..c2c760efa404 100644 --- a/utils/torch_utils.py +++ b/utils/torch_utils.py @@ -34,7 +34,8 @@ def smart_inference_mode(torch_1_9=check_version(torch.__version__, "1.9.0")): - # Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator + """Applies torch.inference_mode() if torch>=1.9.0, else torch.no_grad() as a decorator for functions.""" + def decorate(fn): return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn) @@ -42,7 +43,9 @@ def decorate(fn): def smartCrossEntropyLoss(label_smoothing=0.0): - # Returns nn.CrossEntropyLoss with label smoothing enabled for torch>=1.10.0 + """Returns a CrossEntropyLoss with optional label smoothing for torch>=1.10.0; warns if smoothing on lower + versions. + """ if check_version(torch.__version__, "1.10.0"): return nn.CrossEntropyLoss(label_smoothing=label_smoothing) if label_smoothing > 0: @@ -51,7 +54,7 @@ def smartCrossEntropyLoss(label_smoothing=0.0): def smart_DDP(model): - # Model DDP creation with checks + """Initializes DistributedDataParallel (DDP) for model training, respecting torch version constraints.""" assert not check_version(torch.__version__, "1.12.0", pinned=True), ( "torch==1.12.0 torchvision==0.13.0 DDP training is not supported due to a known issue. " "Please upgrade or downgrade torch to use DDP. See https://github.com/ultralytics/yolov5/issues/8395" @@ -63,7 +66,7 @@ def smart_DDP(model): def reshape_classifier_output(model, n=1000): - # Update a TorchVision classification model to class count 'n' if required + """Reshapes last layer of model to match class count 'n', supporting Classify, Linear, Sequential types.""" from models.common import Classify name, m = list((model.model if hasattr(model, "model") else model).named_children())[-1] # last module @@ -87,7 +90,9 @@ def reshape_classifier_output(model, n=1000): @contextmanager def torch_distributed_zero_first(local_rank: int): - # Decorator to make all processes in distributed training wait for each local_master to do something + """Context manager ensuring ordered operations in distributed training by making all processes wait for the leading + process. + """ if local_rank not in [-1, 0]: dist.barrier(device_ids=[local_rank]) yield @@ -96,7 +101,7 @@ def torch_distributed_zero_first(local_rank: int): def device_count(): - # Returns number of CUDA devices available. Safe version of torch.cuda.device_count(). Supports Linux and Windows + """Returns the number of available CUDA devices; works on Linux and Windows by invoking `nvidia-smi`.""" assert platform.system() in ("Linux", "Windows"), "device_count() only supported on Linux or Windows" try: cmd = "nvidia-smi -L | wc -l" if platform.system() == "Linux" else 'nvidia-smi -L | find /c /v ""' # Windows @@ -106,7 +111,7 @@ def device_count(): def select_device(device="", batch_size=0, newline=True): - # device = None or 'cpu' or 0 or '0' or '0,1,2,3' + """Selects computing device (CPU, CUDA GPU, MPS) for YOLOv5 model deployment, logging device info.""" s = f"YOLOv5 🚀 {git_describe() or file_date()} Python-{platform.python_version()} torch-{torch.__version__} " device = str(device).strip().lower().replace("cuda:", "").replace("none", "") # to string, 'cuda:0' to '0' cpu = device == "cpu" @@ -143,7 +148,7 @@ def select_device(device="", batch_size=0, newline=True): def time_sync(): - # PyTorch-accurate time + """Synchronizes PyTorch for accurate timing, leveraging CUDA if available, and returns the current time.""" if torch.cuda.is_available(): torch.cuda.synchronize() return time.time() @@ -203,16 +208,19 @@ def profile(input, ops, n=10, device=None): def is_parallel(model): - # Returns True if model is of type DP or DDP + """Checks if the model is using Data Parallelism (DP) or Distributed Data Parallelism (DDP).""" return type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel) def de_parallel(model): - # De-parallelize a model: returns single-GPU model if model is of type DP or DDP + """Returns a single-GPU model by removing Data Parallelism (DP) or Distributed Data Parallelism (DDP) if applied.""" return model.module if is_parallel(model) else model def initialize_weights(model): + """Initializes weights of Conv2d, BatchNorm2d, and activations (Hardswish, LeakyReLU, ReLU, ReLU6, SiLU) in the + model. + """ for m in model.modules(): t = type(m) if t is nn.Conv2d: @@ -225,12 +233,14 @@ def initialize_weights(model): def find_modules(model, mclass=nn.Conv2d): - # Finds layer indices matching module class 'mclass' + """Finds and returns list of layer indices in `model.module_list` matching the specified `mclass`.""" return [i for i, m in enumerate(model.module_list) if isinstance(m, mclass)] def sparsity(model): - # Return global model sparsity + """Calculates and returns the global sparsity of a model as the ratio of zero-valued parameters to total + parameters. + """ a, b = 0, 0 for p in model.parameters(): a += p.numel() @@ -239,7 +249,7 @@ def sparsity(model): def prune(model, amount=0.3): - # Prune model to requested global sparsity + """Prunes Conv2d layers in a model to a specified sparsity using L1 unstructured pruning.""" import torch.nn.utils.prune as prune for name, m in model.named_modules(): @@ -250,7 +260,11 @@ def prune(model, amount=0.3): def fuse_conv_and_bn(conv, bn): - # Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/ + """ + Fuses Conv2d and BatchNorm2d layers into a single Conv2d layer. + + See https://tehnokv.com/posts/fusing-batchnorm-and-conv/. + """ fusedconv = ( nn.Conv2d( conv.in_channels, @@ -280,7 +294,11 @@ def fuse_conv_and_bn(conv, bn): def model_info(model, verbose=False, imgsz=640): - # Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320] + """ + Prints model summary including layers, parameters, gradients, and FLOPs; imgsz may be int or list. + + Example: img_size=640 or img_size=[640, 320] + """ n_p = sum(x.numel() for x in model.parameters()) # number parameters n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients if verbose: @@ -319,7 +337,7 @@ def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) def copy_attr(a, b, include=(), exclude=()): - # Copy attributes from b to a, options to only include [...] and to exclude [...] + """Copies attributes from object b to a, optionally filtering with include and exclude lists.""" for k, v in b.__dict__.items(): if (len(include) and k not in include) or k.startswith("_") or k in exclude: continue @@ -328,7 +346,11 @@ def copy_attr(a, b, include=(), exclude=()): def smart_optimizer(model, name="Adam", lr=0.001, momentum=0.9, decay=1e-5): - # YOLOv5 3-param group optimizer: 0) weights with decay, 1) weights no decay, 2) biases no decay + """ + Initializes YOLOv5 smart optimizer with 3 parameter groups for different decay configurations. + + Groups are 0) weights with decay, 1) weights no decay, 2) biases no decay. + """ g = [], [], [] # optimizer parameter groups bn = tuple(v for k, v in nn.__dict__.items() if "Norm" in k) # normalization layers, i.e. BatchNorm2d() for v in model.modules(): @@ -361,7 +383,7 @@ def smart_optimizer(model, name="Adam", lr=0.001, momentum=0.9, decay=1e-5): def smart_hub_load(repo="ultralytics/yolov5", model="yolov5s", **kwargs): - # YOLOv5 torch.hub.load() wrapper with smart error/issue handling + """YOLOv5 torch.hub.load() wrapper with smart error handling, adjusting torch arguments for compatibility.""" if check_version(torch.__version__, "1.9.1"): kwargs["skip_validation"] = True # validation causes GitHub API rate limit errors if check_version(torch.__version__, "1.12.0"): @@ -373,7 +395,7 @@ def smart_hub_load(repo="ultralytics/yolov5", model="yolov5s", **kwargs): def smart_resume(ckpt, optimizer, ema=None, weights="yolov5s.pt", epochs=300, resume=True): - # Resume training from a partially trained checkpoint + """Resumes training from a checkpoint, updating optimizer, ema, and epochs, with optional resume verification.""" best_fitness = 0.0 start_epoch = ckpt["epoch"] + 1 if ckpt["optimizer"] is not None: @@ -397,12 +419,14 @@ def smart_resume(ckpt, optimizer, ema=None, weights="yolov5s.pt", epochs=300, re class EarlyStopping: # YOLOv5 simple early stopper def __init__(self, patience=30): + """Initializes simple early stopping mechanism for YOLOv5, with adjustable patience for non-improving epochs.""" self.best_fitness = 0.0 # i.e. mAP self.best_epoch = 0 self.patience = patience or float("inf") # epochs to wait after fitness stops improving to stop self.possible_stop = False # possible stop may occur next epoch def __call__(self, epoch, fitness): + """Evaluates if training should stop based on fitness improvement and patience, returning a boolean.""" if fitness >= self.best_fitness: # >= 0 to allow for early zero-fitness stage of training self.best_epoch = epoch self.best_fitness = fitness @@ -426,7 +450,9 @@ class ModelEMA: """ def __init__(self, model, decay=0.9999, tau=2000, updates=0): - # Create EMA + """Initializes EMA with model parameters, decay rate, tau for decay adjustment, and update count; sets model to + evaluation mode. + """ self.ema = deepcopy(de_parallel(model)).eval() # FP32 EMA self.updates = updates # number of EMA updates self.decay = lambda x: decay * (1 - math.exp(-x / tau)) # decay exponential ramp (to help early epochs) @@ -434,7 +460,7 @@ def __init__(self, model, decay=0.9999, tau=2000, updates=0): p.requires_grad_(False) def update(self, model): - # Update EMA parameters + """Updates the Exponential Moving Average (EMA) parameters based on the current model's parameters.""" self.updates += 1 d = self.decay(self.updates) @@ -446,5 +472,7 @@ def update(self, model): # assert v.dtype == msd[k].dtype == torch.float32, f'{k}: EMA {v.dtype} and model {msd[k].dtype} must be FP32' def update_attr(self, model, include=(), exclude=("process_group", "reducer")): - # Update EMA attributes + """Updates EMA attributes by copying specified attributes from model to EMA, excluding certain attributes by + default. + """ copy_attr(self.ema, model, include, exclude) diff --git a/utils/triton.py b/utils/triton.py index 9584d07fbcf0..87524c9c7801 100644 --- a/utils/triton.py +++ b/utils/triton.py @@ -71,6 +71,7 @@ def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[t return result[0] if len(result) == 1 else result def _create_inputs(self, *args, **kwargs): + """Creates input tensors from args or kwargs, not both; raises error if none or both are provided.""" args_len, kwargs_len = len(args), len(kwargs) if not args_len and not kwargs_len: raise RuntimeError("No inputs provided.") diff --git a/val.py b/val.py index 6cc1d37a0a26..1c8c65ba89aa 100644 --- a/val.py +++ b/val.py @@ -62,7 +62,7 @@ def save_one_txt(predn, save_conf, shape, file): - # Save one txt result + """Saves one detection result to a txt file in normalized xywh format, optionally including confidence.""" gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh for *xyxy, conf, cls in predn.tolist(): xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh @@ -72,7 +72,11 @@ def save_one_txt(predn, save_conf, shape, file): def save_one_json(predn, jdict, path, class_map): - # Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} + """ + Saves one JSON detection result with image ID, category ID, bounding box, and score. + + Example: {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} + """ image_id = int(path.stem) if path.stem.isnumeric() else path.stem box = xyxy2xywh(predn[:, :4]) # xywh box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner @@ -359,6 +363,7 @@ def run( def parse_opt(): + """Parses command-line options for YOLOv5 model inference configuration.""" parser = argparse.ArgumentParser() parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path") parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s.pt", help="model path(s)") @@ -391,6 +396,9 @@ def parse_opt(): def main(opt): + """Executes YOLOv5 tasks like training, validation, testing, speed, and study benchmarks based on provided + options. + """ check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop")) if opt.task in ("train", "val", "test"): # run normally