\n",
"\n",
"
\n",
- " \n",
+ "
\n",
"\n",
"\n",
"
\n",
+ "
\n",
"
\n",
"
\n",
"
\n",
@@ -402,7 +401,7 @@
"colab": {
"base_uri": "https://localhost:8080/"
},
- "outputId": "0f9ee467-cea4-48e8-9050-7a76ae1b6141"
+ "outputId": "f9f016ad-3dcf-4bd2-e1c3-d5b79efc6f32"
},
"source": [
"!git clone https://github.com/ultralytics/yolov5 # clone\n",
@@ -419,14 +418,14 @@
"output_type": "stream",
"name": "stderr",
"text": [
- "YOLOv5 🚀 v6.2-56-g30e674b Python-3.7.13 torch-1.12.1+cu113 CUDA:0 (Tesla V100-SXM2-16GB, 16160MiB)\n"
+ "YOLOv5 🚀 v7.0-1-gb32f67f Python-3.7.15 torch-1.12.1+cu113 CUDA:0 (Tesla T4, 15110MiB)\n"
]
},
{
"output_type": "stream",
"name": "stdout",
"text": [
- "Setup complete ✅ (8 CPUs, 51.0 GB RAM, 37.4/166.8 GB disk)\n"
+ "Setup complete ✅ (2 CPUs, 12.7 GB RAM, 22.6/78.2 GB disk)\n"
]
}
]
@@ -445,10 +444,11 @@
"python detect.py --source 0 # webcam\n",
" img.jpg # image \n",
" vid.mp4 # video\n",
+ " screen # screenshot\n",
" path/ # directory\n",
- " 'path/*.jpg' # glob\n",
- " 'https://youtu.be/Zgi9g1ksQHc' # YouTube\n",
- " 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream\n",
+ " 'path/*.jpg' # glob\n",
+ " 'https://youtu.be/Zgi9g1ksQHc' # YouTube\n",
+ " 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream\n",
"```"
]
},
@@ -459,7 +459,7 @@
"colab": {
"base_uri": "https://localhost:8080/"
},
- "outputId": "60647b99-e8d4-402c-f444-331bf6746da4"
+ "outputId": "b4db5c49-f501-4505-cf0d-a1d35236c485"
},
"source": [
"!python detect.py --weights yolov5s.pt --img 640 --conf 0.25 --source data/images\n",
@@ -471,17 +471,17 @@
"output_type": "stream",
"name": "stdout",
"text": [
- "\u001b[34m\u001b[1mdetect: \u001b[0mweights=['yolov5s.pt'], source=data/images, data=data/coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs/detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False\n",
- "YOLOv5 🚀 v6.2-56-g30e674b Python-3.7.13 torch-1.12.1+cu113 CUDA:0 (Tesla V100-SXM2-16GB, 16160MiB)\n",
+ "\u001b[34m\u001b[1mdetect: \u001b[0mweights=['yolov5s.pt'], source=data/images, data=data/coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs/detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False, vid_stride=1\n",
+ "YOLOv5 🚀 v7.0-1-gb32f67f Python-3.7.15 torch-1.12.1+cu113 CUDA:0 (Tesla T4, 15110MiB)\n",
"\n",
- "Downloading https://github.com/ultralytics/yolov5/releases/download/v6.2/yolov5s.pt to yolov5s.pt...\n",
- "100% 14.1M/14.1M [00:00<00:00, 27.8MB/s]\n",
+ "Downloading https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5s.pt to yolov5s.pt...\n",
+ "100% 14.1M/14.1M [00:00<00:00, 116MB/s] \n",
"\n",
"Fusing layers... \n",
"YOLOv5s summary: 213 layers, 7225885 parameters, 0 gradients\n",
- "image 1/2 /content/yolov5/data/images/bus.jpg: 640x480 4 persons, 1 bus, 14.8ms\n",
- "image 2/2 /content/yolov5/data/images/zidane.jpg: 384x640 2 persons, 2 ties, 20.1ms\n",
- "Speed: 0.6ms pre-process, 17.4ms inference, 21.6ms NMS per image at shape (1, 3, 640, 640)\n",
+ "image 1/2 /content/yolov5/data/images/bus.jpg: 640x480 4 persons, 1 bus, 17.0ms\n",
+ "image 2/2 /content/yolov5/data/images/zidane.jpg: 384x640 2 persons, 2 ties, 14.3ms\n",
+ "Speed: 0.5ms pre-process, 15.7ms inference, 18.6ms NMS per image at shape (1, 3, 640, 640)\n",
"Results saved to \u001b[1mruns/detect/exp\u001b[0m\n"
]
}
@@ -515,20 +515,20 @@
"base_uri": "https://localhost:8080/",
"height": 49,
"referenced_widgets": [
- "9b8caa3522fc4cbab31e13b5dfc7808d",
- "574140e4c4bc48c9a171541a02cd0211",
- "35e03ce5090346c9ae602891470fc555",
- "c942c208e72d46568b476bb0f2d75496",
- "65881db1db8a4e9c930fab9172d45143",
- "60b913d755b34d638478e30705a2dde1",
- "0856bea36ec148b68522ff9c9eb258d8",
- "76879f6f2aa54637a7a07faeea2bd684",
- "0ace3934ec6f4d36a1b3a9e086390926",
- "d6b7a2243e0c4beca714d99dceec23d6",
- "5966ba6e6f114d8c9d8d1d6b1bd4f4c7"
+ "1f7df330663048998adcf8a45bc8f69b",
+ "e896e6096dd244c59d7955e2035cd729",
+ "a6ff238c29984b24bf6d0bd175c19430",
+ "3c085ba3f3fd4c3c8a6bb41b41ce1479",
+ "16b0c8aa6e0f427e8a54d3791abb7504",
+ "c7b2dd0f78384cad8e400b282996cdf5",
+ "6a27e43b0e434edd82ee63f0a91036ca",
+ "cce0e6c0c4ec442cb47e65c674e02e92",
+ "c5b9f38e2f0d4f9aa97fe87265263743",
+ "df554fb955c7454696beac5a82889386",
+ "74e9112a87a242f4831b7d68c7da6333"
]
},
- "outputId": "102dabed-bc31-42fe-9133-d9ce28a2c01e"
+ "outputId": "c7d0a0d2-abfb-44c3-d60d-f99d0e7aabad"
},
"source": [
"# Download COCO val\n",
@@ -546,7 +546,7 @@
"application/vnd.jupyter.widget-view+json": {
"version_major": 2,
"version_minor": 0,
- "model_id": "9b8caa3522fc4cbab31e13b5dfc7808d"
+ "model_id": "1f7df330663048998adcf8a45bc8f69b"
}
},
"metadata": {}
@@ -560,7 +560,7 @@
"colab": {
"base_uri": "https://localhost:8080/"
},
- "outputId": "daf60b1b-b098-4657-c863-584f4c9cf078"
+ "outputId": "5fc61358-7bc5-4310-a310-9059f66c6322"
},
"source": [
"# Validate YOLOv5s on COCO val\n",
@@ -572,33 +572,31 @@
"output_type": "stream",
"name": "stdout",
"text": [
- "\u001b[34m\u001b[1mval: \u001b[0mdata=/content/yolov5/data/coco.yaml, weights=['yolov5s.pt'], batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, task=val, device=, workers=8, single_cls=False, augment=False, verbose=False, save_txt=False, save_hybrid=False, save_conf=False, save_json=True, project=runs/val, name=exp, exist_ok=False, half=True, dnn=False\n",
- "YOLOv5 🚀 v6.2-56-g30e674b Python-3.7.13 torch-1.12.1+cu113 CUDA:0 (Tesla V100-SXM2-16GB, 16160MiB)\n",
+ "\u001b[34m\u001b[1mval: \u001b[0mdata=/content/yolov5/data/coco.yaml, weights=['yolov5s.pt'], batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, max_det=300, task=val, device=, workers=8, single_cls=False, augment=False, verbose=False, save_txt=False, save_hybrid=False, save_conf=False, save_json=True, project=runs/val, name=exp, exist_ok=False, half=True, dnn=False\n",
+ "YOLOv5 🚀 v7.0-1-gb32f67f Python-3.7.15 torch-1.12.1+cu113 CUDA:0 (Tesla T4, 15110MiB)\n",
"\n",
"Fusing layers... \n",
"YOLOv5s summary: 213 layers, 7225885 parameters, 0 gradients\n",
- "Downloading https://ultralytics.com/assets/Arial.ttf to /root/.config/Ultralytics/Arial.ttf...\n",
- "100% 755k/755k [00:00<00:00, 52.7MB/s]\n",
- "\u001b[34m\u001b[1mval: \u001b[0mScanning '/content/datasets/coco/val2017' images and labels...4952 found, 48 missing, 0 empty, 0 corrupt: 100% 5000/5000 [00:00<00:00, 10509.20it/s]\n",
+ "\u001b[34m\u001b[1mval: \u001b[0mScanning /content/datasets/coco/val2017... 4952 images, 48 backgrounds, 0 corrupt: 100% 5000/5000 [00:02<00:00, 1977.30it/s]\n",
"\u001b[34m\u001b[1mval: \u001b[0mNew cache created: /content/datasets/coco/val2017.cache\n",
- " Class Images Instances P R mAP@.5 mAP@.5:.95: 100% 157/157 [00:50<00:00, 3.10it/s]\n",
+ " Class Images Instances P R mAP50 mAP50-95: 100% 157/157 [01:12<00:00, 2.17it/s]\n",
" all 5000 36335 0.67 0.521 0.566 0.371\n",
- "Speed: 0.1ms pre-process, 1.0ms inference, 1.5ms NMS per image at shape (32, 3, 640, 640)\n",
+ "Speed: 0.1ms pre-process, 2.9ms inference, 2.0ms NMS per image at shape (32, 3, 640, 640)\n",
"\n",
"Evaluating pycocotools mAP... saving runs/val/exp/yolov5s_predictions.json...\n",
"loading annotations into memory...\n",
- "Done (t=0.81s)\n",
+ "Done (t=0.43s)\n",
"creating index...\n",
"index created!\n",
"Loading and preparing results...\n",
- "DONE (t=5.62s)\n",
+ "DONE (t=5.85s)\n",
"creating index...\n",
"index created!\n",
"Running per image evaluation...\n",
"Evaluate annotation type *bbox*\n",
- "DONE (t=77.03s).\n",
+ "DONE (t=82.22s).\n",
"Accumulating evaluation results...\n",
- "DONE (t=14.63s).\n",
+ "DONE (t=14.92s).\n",
" Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.374\n",
" Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.572\n",
" Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.402\n",
@@ -610,7 +608,7 @@
" Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.566\n",
" Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.378\n",
" Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.625\n",
- " Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.724\n",
+ " Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.723\n",
"Results saved to \u001b[1mruns/val/exp\u001b[0m\n"
]
}
@@ -624,7 +622,7 @@
"source": [
"# 3. Train\n",
"\n",
- "
\n",
+ "
\n",
"Close the active learning loop by sampling images from your inference conditions with the `roboflow` pip package\n",
"
\n",
"\n",
@@ -653,19 +651,17 @@
"cell_type": "code",
"source": [
"#@title Select YOLOv5 🚀 logger {run: 'auto'}\n",
- "logger = 'TensorBoard' #@param ['TensorBoard', 'Comet', 'ClearML', 'W&B']\n",
+ "logger = 'ClearML' #@param ['ClearML', 'Comet', 'TensorBoard']\n",
"\n",
- "if logger == 'TensorBoard':\n",
- " %load_ext tensorboard\n",
- " %tensorboard --logdir runs/train\n",
+ "if logger == 'ClearML':\n",
+ " %pip install -q clearml\n",
+ " import clearml; clearml.browser_login()\n",
"elif logger == 'Comet':\n",
" %pip install -q comet_ml\n",
" import comet_ml; comet_ml.init()\n",
- "elif logger == 'ClearML':\n",
- " %pip install -q clearml && clearml-init\n",
- "elif logger == 'W&B':\n",
- " %pip install -q wandb\n",
- " import wandb; wandb.login()"
+ "elif logger == 'TensorBoard':\n",
+ " %load_ext tensorboard\n",
+ " %tensorboard --logdir runs/train"
],
"metadata": {
"id": "i3oKtE4g-aNn"
@@ -680,7 +676,7 @@
"colab": {
"base_uri": "https://localhost:8080/"
},
- "outputId": "baa6d4be-3379-4aab-844a-d5a5396c0e49"
+ "outputId": "721b9028-767f-4a05-c964-692c245f7398"
},
"source": [
"# Train YOLOv5s on COCO128 for 3 epochs\n",
@@ -694,17 +690,17 @@
"text": [
"\u001b[34m\u001b[1mtrain: \u001b[0mweights=yolov5s.pt, cfg=, data=coco128.yaml, hyp=data/hyps/hyp.scratch-low.yaml, epochs=3, batch_size=16, imgsz=640, rect=False, resume=False, nosave=False, noval=False, noautoanchor=False, noplots=False, evolve=None, bucket=, cache=ram, image_weights=False, device=, multi_scale=False, single_cls=False, optimizer=SGD, sync_bn=False, workers=8, project=runs/train, name=exp, exist_ok=False, quad=False, cos_lr=False, label_smoothing=0.0, patience=100, freeze=[0], save_period=-1, seed=0, local_rank=-1, entity=None, upload_dataset=False, bbox_interval=-1, artifact_alias=latest\n",
"\u001b[34m\u001b[1mgithub: \u001b[0mup to date with https://github.com/ultralytics/yolov5 ✅\n",
- "YOLOv5 🚀 v6.2-56-g30e674b Python-3.7.13 torch-1.12.1+cu113 CUDA:0 (Tesla V100-SXM2-16GB, 16160MiB)\n",
+ "YOLOv5 🚀 v7.0-1-gb32f67f Python-3.7.15 torch-1.12.1+cu113 CUDA:0 (Tesla T4, 15110MiB)\n",
"\n",
"\u001b[34m\u001b[1mhyperparameters: \u001b[0mlr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0\n",
- "\u001b[34m\u001b[1mWeights & Biases: \u001b[0mrun 'pip install wandb' to automatically track and visualize YOLOv5 🚀 runs in Weights & Biases\n",
"\u001b[34m\u001b[1mClearML: \u001b[0mrun 'pip install clearml' to automatically track, visualize and remotely train YOLOv5 🚀 in ClearML\n",
+ "\u001b[34m\u001b[1mComet: \u001b[0mrun 'pip install comet_ml' to automatically track and visualize YOLOv5 🚀 runs in Comet\n",
"\u001b[34m\u001b[1mTensorBoard: \u001b[0mStart with 'tensorboard --logdir runs/train', view at http://localhost:6006/\n",
"\n",
"Dataset not found ⚠️, missing paths ['/content/datasets/coco128/images/train2017']\n",
"Downloading https://ultralytics.com/assets/coco128.zip to coco128.zip...\n",
- "100% 6.66M/6.66M [00:00<00:00, 41.1MB/s]\n",
- "Dataset download success ✅ (0.8s), saved to \u001b[1m/content/datasets\u001b[0m\n",
+ "100% 6.66M/6.66M [00:00<00:00, 261MB/s]\n",
+ "Dataset download success ✅ (0.3s), saved to \u001b[1m/content/datasets\u001b[0m\n",
"\n",
" from n params module arguments \n",
" 0 -1 1 3520 models.common.Conv [3, 32, 6, 2, 2] \n",
@@ -732,120 +728,120 @@
" 22 [-1, 10] 1 0 models.common.Concat [1] \n",
" 23 -1 1 1182720 models.common.C3 [512, 512, 1, False] \n",
" 24 [17, 20, 23] 1 229245 models.yolo.Detect [80, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]\n",
- "Model summary: 270 layers, 7235389 parameters, 7235389 gradients, 16.6 GFLOPs\n",
+ "Model summary: 214 layers, 7235389 parameters, 7235389 gradients, 16.6 GFLOPs\n",
"\n",
"Transferred 349/349 items from yolov5s.pt\n",
"\u001b[34m\u001b[1mAMP: \u001b[0mchecks passed ✅\n",
"\u001b[34m\u001b[1moptimizer:\u001b[0m SGD(lr=0.01) with parameter groups 57 weight(decay=0.0), 60 weight(decay=0.0005), 60 bias\n",
"\u001b[34m\u001b[1malbumentations: \u001b[0mBlur(p=0.01, blur_limit=(3, 7)), MedianBlur(p=0.01, blur_limit=(3, 7)), ToGray(p=0.01), CLAHE(p=0.01, clip_limit=(1, 4.0), tile_grid_size=(8, 8))\n",
- "\u001b[34m\u001b[1mtrain: \u001b[0mScanning '/content/datasets/coco128/labels/train2017' images and labels...128 found, 0 missing, 2 empty, 0 corrupt: 100% 128/128 [00:00<00:00, 9659.25it/s]\n",
+ "\u001b[34m\u001b[1mtrain: \u001b[0mScanning /content/datasets/coco128/labels/train2017... 126 images, 2 backgrounds, 0 corrupt: 100% 128/128 [00:00<00:00, 1911.57it/s]\n",
"\u001b[34m\u001b[1mtrain: \u001b[0mNew cache created: /content/datasets/coco128/labels/train2017.cache\n",
- "\u001b[34m\u001b[1mtrain: \u001b[0mCaching images (0.1GB ram): 100% 128/128 [00:00<00:00, 951.31it/s]\n",
- "\u001b[34m\u001b[1mval: \u001b[0mScanning '/content/datasets/coco128/labels/train2017.cache' images and labels... 128 found, 0 missing, 2 empty, 0 corrupt: 100% 128/128 [00:00, ?it/s]\n",
- "\u001b[34m\u001b[1mval: \u001b[0mCaching images (0.1GB ram): 100% 128/128 [00:00<00:00, 274.67it/s]\n",
+ "\u001b[34m\u001b[1mtrain: \u001b[0mCaching images (0.1GB ram): 100% 128/128 [00:00<00:00, 229.69it/s]\n",
+ "\u001b[34m\u001b[1mval: \u001b[0mScanning /content/datasets/coco128/labels/train2017.cache... 126 images, 2 backgrounds, 0 corrupt: 100% 128/128 [00:00, ?it/s]\n",
+ "\u001b[34m\u001b[1mval: \u001b[0mCaching images (0.1GB ram): 100% 128/128 [00:01<00:00, 97.70it/s] \n",
"\n",
"\u001b[34m\u001b[1mAutoAnchor: \u001b[0m4.27 anchors/target, 0.994 Best Possible Recall (BPR). Current anchors are a good fit to dataset ✅\n",
"Plotting labels to runs/train/exp/labels.jpg... \n",
"Image sizes 640 train, 640 val\n",
- "Using 8 dataloader workers\n",
+ "Using 2 dataloader workers\n",
"Logging results to \u001b[1mruns/train/exp\u001b[0m\n",
"Starting training for 3 epochs...\n",
"\n",
" Epoch GPU_mem box_loss obj_loss cls_loss Instances Size\n",
- " 0/2 3.44G 0.04529 0.06712 0.01835 323 640: 100% 8/8 [00:04<00:00, 1.71it/s]\n",
- " Class Images Instances P R mAP@.5 mAP@.5:.95: 100% 4/4 [00:00<00:00, 4.02it/s]\n",
- " all 128 929 0.666 0.611 0.684 0.452\n",
+ " 0/2 3.74G 0.04618 0.07207 0.017 232 640: 100% 8/8 [00:07<00:00, 1.10it/s]\n",
+ " Class Images Instances P R mAP50 mAP50-95: 100% 4/4 [00:01<00:00, 2.28it/s]\n",
+ " all 128 929 0.672 0.594 0.682 0.451\n",
"\n",
" Epoch GPU_mem box_loss obj_loss cls_loss Instances Size\n",
- " 1/2 4.46G 0.04244 0.06423 0.01611 236 640: 100% 8/8 [00:01<00:00, 7.91it/s]\n",
- " Class Images Instances P R mAP@.5 mAP@.5:.95: 100% 4/4 [00:00<00:00, 4.19it/s]\n",
- " all 128 929 0.746 0.626 0.722 0.481\n",
+ " 1/2 5.36G 0.04623 0.06888 0.01821 201 640: 100% 8/8 [00:02<00:00, 3.29it/s]\n",
+ " Class Images Instances P R mAP50 mAP50-95: 100% 4/4 [00:01<00:00, 3.17it/s]\n",
+ " all 128 929 0.721 0.639 0.724 0.48\n",
"\n",
" Epoch GPU_mem box_loss obj_loss cls_loss Instances Size\n",
- " 2/2 4.46G 0.04695 0.06875 0.0173 189 640: 100% 8/8 [00:00<00:00, 8.05it/s]\n",
- " Class Images Instances P R mAP@.5 mAP@.5:.95: 100% 4/4 [00:00<00:00, 4.29it/s]\n",
- " all 128 929 0.774 0.647 0.746 0.499\n",
+ " 2/2 5.36G 0.04361 0.06479 0.01698 227 640: 100% 8/8 [00:02<00:00, 3.46it/s]\n",
+ " Class Images Instances P R mAP50 mAP50-95: 100% 4/4 [00:01<00:00, 3.11it/s]\n",
+ " all 128 929 0.758 0.641 0.731 0.487\n",
"\n",
- "3 epochs completed in 0.003 hours.\n",
+ "3 epochs completed in 0.005 hours.\n",
"Optimizer stripped from runs/train/exp/weights/last.pt, 14.9MB\n",
"Optimizer stripped from runs/train/exp/weights/best.pt, 14.9MB\n",
"\n",
"Validating runs/train/exp/weights/best.pt...\n",
"Fusing layers... \n",
- "Model summary: 213 layers, 7225885 parameters, 0 gradients, 16.4 GFLOPs\n",
- " Class Images Instances P R mAP@.5 mAP@.5:.95: 100% 4/4 [00:03<00:00, 1.21it/s]\n",
- " all 128 929 0.774 0.647 0.746 0.499\n",
- " person 128 254 0.87 0.697 0.806 0.534\n",
- " bicycle 128 6 0.759 0.528 0.725 0.444\n",
- " car 128 46 0.774 0.413 0.554 0.239\n",
- " motorcycle 128 5 0.791 1 0.962 0.595\n",
- " airplane 128 6 0.981 1 0.995 0.689\n",
- " bus 128 7 0.65 0.714 0.755 0.691\n",
- " train 128 3 1 0.573 0.995 0.602\n",
- " truck 128 12 0.613 0.333 0.489 0.263\n",
- " boat 128 6 0.933 0.333 0.507 0.209\n",
- " traffic light 128 14 0.76 0.228 0.367 0.209\n",
- " stop sign 128 2 0.821 1 0.995 0.821\n",
- " bench 128 9 0.824 0.526 0.676 0.31\n",
- " bird 128 16 0.974 1 0.995 0.611\n",
- " cat 128 4 0.859 1 0.995 0.772\n",
- " dog 128 9 1 0.666 0.883 0.647\n",
- " horse 128 2 0.84 1 0.995 0.622\n",
- " elephant 128 17 0.926 0.882 0.93 0.716\n",
- " bear 128 1 0.709 1 0.995 0.995\n",
- " zebra 128 4 0.866 1 0.995 0.922\n",
- " giraffe 128 9 0.777 0.778 0.891 0.705\n",
- " backpack 128 6 0.894 0.5 0.753 0.294\n",
- " umbrella 128 18 0.876 0.783 0.899 0.54\n",
- " handbag 128 19 0.799 0.209 0.335 0.179\n",
- " tie 128 7 0.782 0.714 0.787 0.478\n",
- " suitcase 128 4 0.658 1 0.945 0.581\n",
- " frisbee 128 5 0.726 0.8 0.76 0.701\n",
- " skis 128 1 0.8 1 0.995 0.103\n",
- " snowboard 128 7 0.815 0.714 0.852 0.574\n",
- " sports ball 128 6 0.649 0.667 0.602 0.307\n",
- " kite 128 10 0.7 0.47 0.546 0.206\n",
- " baseball bat 128 4 1 0.497 0.544 0.182\n",
- " baseball glove 128 7 0.598 0.429 0.47 0.31\n",
- " skateboard 128 5 0.851 0.6 0.685 0.495\n",
- " tennis racket 128 7 0.754 0.429 0.544 0.34\n",
- " bottle 128 18 0.564 0.333 0.53 0.264\n",
- " wine glass 128 16 0.715 0.875 0.907 0.528\n",
- " cup 128 36 0.825 0.639 0.803 0.535\n",
- " fork 128 6 1 0.329 0.5 0.384\n",
- " knife 128 16 0.706 0.625 0.666 0.405\n",
- " spoon 128 22 0.836 0.464 0.619 0.379\n",
- " bowl 128 28 0.763 0.607 0.717 0.516\n",
- " banana 128 1 0.886 1 0.995 0.399\n",
- " sandwich 128 2 1 0 0.62 0.546\n",
- " orange 128 4 1 0.75 0.995 0.622\n",
- " broccoli 128 11 0.548 0.443 0.467 0.35\n",
- " carrot 128 24 0.7 0.585 0.699 0.458\n",
- " hot dog 128 2 0.502 1 0.995 0.995\n",
- " pizza 128 5 0.813 1 0.962 0.747\n",
- " donut 128 14 0.662 1 0.96 0.838\n",
- " cake 128 4 0.868 1 0.995 0.822\n",
- " chair 128 35 0.538 0.571 0.594 0.322\n",
- " couch 128 6 0.924 0.667 0.828 0.538\n",
- " potted plant 128 14 0.731 0.786 0.824 0.495\n",
- " bed 128 3 0.736 0.333 0.83 0.425\n",
- " dining table 128 13 0.624 0.259 0.494 0.336\n",
- " toilet 128 2 0.79 1 0.995 0.846\n",
- " tv 128 2 0.574 1 0.995 0.796\n",
- " laptop 128 3 1 0 0.695 0.367\n",
- " mouse 128 2 1 0 0.173 0.0864\n",
- " remote 128 8 1 0.62 0.634 0.557\n",
- " cell phone 128 8 0.612 0.397 0.437 0.221\n",
- " microwave 128 3 0.741 1 0.995 0.766\n",
- " oven 128 5 0.33 0.4 0.449 0.3\n",
- " sink 128 6 0.444 0.333 0.331 0.231\n",
- " refrigerator 128 5 0.561 0.8 0.798 0.546\n",
- " book 128 29 0.635 0.276 0.355 0.164\n",
- " clock 128 9 0.766 0.889 0.888 0.73\n",
- " vase 128 2 0.303 1 0.995 0.895\n",
- " scissors 128 1 1 0 0.332 0.0397\n",
- " teddy bear 128 21 0.842 0.508 0.739 0.499\n",
- " toothbrush 128 5 0.787 1 0.928 0.59\n",
+ "Model summary: 157 layers, 7225885 parameters, 0 gradients, 16.4 GFLOPs\n",
+ " Class Images Instances P R mAP50 mAP50-95: 100% 4/4 [00:03<00:00, 1.05it/s]\n",
+ " all 128 929 0.757 0.641 0.732 0.487\n",
+ " person 128 254 0.86 0.705 0.804 0.528\n",
+ " bicycle 128 6 0.773 0.578 0.725 0.426\n",
+ " car 128 46 0.658 0.435 0.554 0.239\n",
+ " motorcycle 128 5 0.59 0.8 0.837 0.635\n",
+ " airplane 128 6 1 0.996 0.995 0.696\n",
+ " bus 128 7 0.635 0.714 0.756 0.666\n",
+ " train 128 3 0.691 0.333 0.753 0.511\n",
+ " truck 128 12 0.604 0.333 0.472 0.26\n",
+ " boat 128 6 0.941 0.333 0.46 0.183\n",
+ " traffic light 128 14 0.557 0.183 0.302 0.214\n",
+ " stop sign 128 2 0.827 1 0.995 0.846\n",
+ " bench 128 9 0.79 0.556 0.677 0.318\n",
+ " bird 128 16 0.962 1 0.995 0.663\n",
+ " cat 128 4 0.867 1 0.995 0.754\n",
+ " dog 128 9 1 0.649 0.903 0.654\n",
+ " horse 128 2 0.853 1 0.995 0.622\n",
+ " elephant 128 17 0.908 0.882 0.934 0.698\n",
+ " bear 128 1 0.697 1 0.995 0.995\n",
+ " zebra 128 4 0.867 1 0.995 0.905\n",
+ " giraffe 128 9 0.788 0.829 0.912 0.701\n",
+ " backpack 128 6 0.841 0.5 0.738 0.311\n",
+ " umbrella 128 18 0.786 0.815 0.859 0.48\n",
+ " handbag 128 19 0.772 0.263 0.366 0.216\n",
+ " tie 128 7 0.975 0.714 0.77 0.491\n",
+ " suitcase 128 4 0.643 0.75 0.912 0.563\n",
+ " frisbee 128 5 0.72 0.8 0.76 0.717\n",
+ " skis 128 1 0.748 1 0.995 0.3\n",
+ " snowboard 128 7 0.827 0.686 0.833 0.57\n",
+ " sports ball 128 6 0.637 0.667 0.602 0.311\n",
+ " kite 128 10 0.645 0.6 0.594 0.224\n",
+ " baseball bat 128 4 0.519 0.278 0.468 0.205\n",
+ " baseball glove 128 7 0.483 0.429 0.465 0.278\n",
+ " skateboard 128 5 0.923 0.6 0.687 0.493\n",
+ " tennis racket 128 7 0.774 0.429 0.544 0.333\n",
+ " bottle 128 18 0.577 0.379 0.551 0.275\n",
+ " wine glass 128 16 0.715 0.875 0.893 0.511\n",
+ " cup 128 36 0.843 0.667 0.833 0.531\n",
+ " fork 128 6 0.998 0.333 0.45 0.315\n",
+ " knife 128 16 0.77 0.688 0.695 0.399\n",
+ " spoon 128 22 0.839 0.473 0.638 0.383\n",
+ " bowl 128 28 0.765 0.583 0.715 0.512\n",
+ " banana 128 1 0.903 1 0.995 0.301\n",
+ " sandwich 128 2 1 0 0.359 0.301\n",
+ " orange 128 4 0.718 0.75 0.912 0.581\n",
+ " broccoli 128 11 0.545 0.364 0.43 0.319\n",
+ " carrot 128 24 0.62 0.625 0.724 0.495\n",
+ " hot dog 128 2 0.385 1 0.828 0.762\n",
+ " pizza 128 5 0.833 1 0.962 0.725\n",
+ " donut 128 14 0.631 1 0.96 0.833\n",
+ " cake 128 4 0.871 1 0.995 0.83\n",
+ " chair 128 35 0.583 0.6 0.608 0.318\n",
+ " couch 128 6 0.909 0.667 0.813 0.543\n",
+ " potted plant 128 14 0.745 0.786 0.822 0.48\n",
+ " bed 128 3 0.973 0.333 0.753 0.41\n",
+ " dining table 128 13 0.821 0.356 0.577 0.342\n",
+ " toilet 128 2 1 0.949 0.995 0.797\n",
+ " tv 128 2 0.566 1 0.995 0.796\n",
+ " laptop 128 3 1 0 0.59 0.311\n",
+ " mouse 128 2 1 0 0.105 0.0527\n",
+ " remote 128 8 1 0.623 0.634 0.538\n",
+ " cell phone 128 8 0.565 0.375 0.399 0.179\n",
+ " microwave 128 3 0.709 1 0.995 0.736\n",
+ " oven 128 5 0.328 0.4 0.43 0.282\n",
+ " sink 128 6 0.438 0.333 0.339 0.266\n",
+ " refrigerator 128 5 0.564 0.8 0.798 0.535\n",
+ " book 128 29 0.597 0.256 0.351 0.155\n",
+ " clock 128 9 0.763 0.889 0.934 0.737\n",
+ " vase 128 2 0.331 1 0.995 0.895\n",
+ " scissors 128 1 1 0 0.497 0.0552\n",
+ " teddy bear 128 21 0.857 0.57 0.837 0.544\n",
+ " toothbrush 128 5 0.799 1 0.928 0.556\n",
"Results saved to \u001b[1mruns/train/exp\u001b[0m\n"
]
}
@@ -864,7 +860,8 @@
"cell_type": "markdown",
"source": [
"## Comet Logging and Visualization 🌟 NEW\n",
- "[Comet](https://www.comet.com/site/?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration) is now fully integrated with YOLOv5. Track and visualize model metrics in real time, save your hyperparameters, datasets, and model checkpoints, and visualize your model predictions with [Comet Custom Panels](https://www.comet.com/docs/v2/guides/comet-dashboard/code-panels/about-panels/?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)! Comet makes sure you never lose track of your work and makes it easy to share results and collaborate across teams of all sizes! \n",
+ "\n",
+ "[Comet](https://www.comet.com/site/lp/yolov5-with-comet/?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=yolov5_colab) is now fully integrated with YOLOv5. Track and visualize model metrics in real time, save your hyperparameters, datasets, and model checkpoints, and visualize your model predictions with [Comet Custom Panels](https://www.comet.com/docs/v2/guides/comet-dashboard/code-panels/about-panels/?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=yolov5_colab)! Comet makes sure you never lose track of your work and makes it easy to share results and collaborate across teams of all sizes!\n",
"\n",
"Getting started is easy:\n",
"```shell\n",
@@ -872,11 +869,11 @@
"export COMET_API_KEY=
# 2. paste API key\n",
"python train.py --img 640 --epochs 3 --data coco128.yaml --weights yolov5s.pt # 3. train\n",
"```\n",
- "\n",
- "To learn more about all of the supported Comet features for this integration, check out the [Comet Tutorial](https://github.com/ultralytics/yolov5/tree/master/utils/loggers/comet). If you'd like to learn more about Comet, head over to our [documentation](https://www.comet.com/docs/v2/?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration). Get started by trying out the Comet Colab Notebook:\n",
+ "To learn more about all of the supported Comet features for this integration, check out the [Comet Tutorial](https://github.com/ultralytics/yolov5/tree/master/utils/loggers/comet). If you'd like to learn more about Comet, head over to our [documentation](https://www.comet.com/docs/v2/?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=yolov5_colab). Get started by trying out the Comet Colab Notebook:\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1RG0WOQyxlDlo5Km8GogJpIEJlg_5lyYO?usp=sharing)\n",
"\n",
- ""
+ "\n",
+ ""
],
"metadata": {
"id": "nWOsI5wJR1o3"
@@ -903,22 +900,6 @@
"id": "Lay2WsTjNJzP"
}
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "DLI1JmHU7B0l"
- },
- "source": [
- "## Weights & Biases Logging\n",
- "\n",
- "[Weights & Biases](https://wandb.ai/site?utm_campaign=repo_yolo_notebook) (W&B) is integrated with YOLOv5 for real-time visualization and cloud logging of training runs. This allows for better run comparison and introspection, as well improved visibility and collaboration for teams. To enable W&B `pip install wandb`, and then train normally (you will be guided through setup on first use). \n",
- "\n",
- "During training you will see live updates at [https://wandb.ai/home](https://wandb.ai/home?utm_campaign=repo_yolo_notebook), and you can create and share detailed [Reports](https://wandb.ai/glenn-jocher/yolov5_tutorial/reports/YOLOv5-COCO128-Tutorial-Results--VmlldzozMDI5OTY) of your results. For more information see the [YOLOv5 Weights & Biases Tutorial](https://github.com/ultralytics/yolov5/issues/1289). \n",
- "\n",
- "\n",
- ""
- ]
- },
{
"cell_type": "markdown",
"metadata": {
@@ -944,7 +925,7 @@
"\n",
"YOLOv5 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled):\n",
"\n",
- "- **Google Colab and Kaggle** notebooks with free GPU: \n",
+ "- **Notebooks** with free GPU: \n",
"- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/GCP-Quickstart)\n",
"- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/AWS-Quickstart)\n",
"- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/Docker-Quickstart) \n"
@@ -958,7 +939,7 @@
"source": [
"# Status\n",
"\n",
- "![CI CPU testing](https://github.com/ultralytics/yolov5/workflows/CI%20CPU%20testing/badge.svg)\n",
+ "![YOLOv5 CI](https://github.com/ultralytics/yolov5/actions/workflows/ci-testing.yml/badge.svg)\n",
"\n",
"If this badge is green, all [YOLOv5 GitHub Actions](https://github.com/ultralytics/yolov5/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv5 training ([train.py](https://github.com/ultralytics/yolov5/blob/master/train.py)), testing ([val.py](https://github.com/ultralytics/yolov5/blob/master/val.py)), inference ([detect.py](https://github.com/ultralytics/yolov5/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov5/blob/master/export.py)) on macOS, Windows, and Ubuntu every 24 hours and on every commit.\n"
]
@@ -971,7 +952,7 @@
"source": [
"# Appendix\n",
"\n",
- "Additional content below for PyTorch Hub, CI, reproducing results, profiling speeds, VOC training, classification training and TensorRT example."
+ "Additional content below."
]
},
{
@@ -980,145 +961,16 @@
"id": "GMusP4OAxFu6"
},
"source": [
+ "# YOLOv5 PyTorch HUB Inference (DetectionModels only)\n",
"import torch\n",
"\n",
- "# PyTorch Hub Model\n",
- "model = torch.hub.load('ultralytics/yolov5', 'yolov5s') # or yolov5n - yolov5x6, custom\n",
- "\n",
- "# Images\n",
- "img = 'https://ultralytics.com/images/zidane.jpg' # or file, Path, PIL, OpenCV, numpy, list\n",
- "\n",
- "# Inference\n",
- "results = model(img)\n",
- "\n",
- "# Results\n",
+ "model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=True) # yolov5n - yolov5x6 or custom\n",
+ "im = 'https://ultralytics.com/images/zidane.jpg' # file, Path, PIL.Image, OpenCV, nparray, list\n",
+ "results = model(im) # inference\n",
"results.print() # or .show(), .save(), .crop(), .pandas(), etc."
],
"execution_count": null,
"outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "FGH0ZjkGjejy"
- },
- "source": [
- "# YOLOv5 CI\n",
- "%%shell\n",
- "rm -rf runs # remove runs/\n",
- "m=yolov5n # official weights\n",
- "b=runs/train/exp/weights/best # best.pt checkpoint\n",
- "python train.py --imgsz 64 --batch 32 --weights $m.pt --cfg $m.yaml --epochs 1 --device 0 # train\n",
- "for d in 0 cpu; do # devices\n",
- " for w in $m $b; do # weights\n",
- " python val.py --imgsz 64 --batch 32 --weights $w.pt --device $d # val\n",
- " python detect.py --imgsz 64 --weights $w.pt --device $d # detect\n",
- " done\n",
- "done\n",
- "python hubconf.py --model $m # hub\n",
- "python models/tf.py --weights $m.pt # build TF model\n",
- "python models/yolo.py --cfg $m.yaml # build PyTorch model\n",
- "python export.py --weights $m.pt --img 64 --include torchscript # export"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "mcKoSIK2WSzj"
- },
- "source": [
- "# Reproduce\n",
- "for x in (f'yolov5{x}' for x in 'nsmlx'):\n",
- " !python val.py --weights {x}.pt --data coco.yaml --img 640 --task speed # speed\n",
- " !python val.py --weights {x}.pt --data coco.yaml --img 640 --conf 0.001 --iou 0.65 # mAP"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "gogI-kwi3Tye"
- },
- "source": [
- "# Profile\n",
- "from utils.torch_utils import profile\n",
- "\n",
- "m1 = lambda x: x * torch.sigmoid(x)\n",
- "m2 = torch.nn.SiLU()\n",
- "results = profile(input=torch.randn(16, 3, 640, 640), ops=[m1, m2], n=100)"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "BSgFCAcMbk1R"
- },
- "source": [
- "# VOC\n",
- "for b, m in zip([64, 64, 64, 32, 16], [f'yolov5{x}' for x in 'nsmlx']): # batch, model\n",
- " !python train.py --batch {b} --weights {m}.pt --data VOC.yaml --epochs 50 --img 512 --hyp hyp.VOC.yaml --project VOC --name {m} --cache"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "# Classification train\n",
- "for m in [*(f'yolov5{x}-cls.pt' for x in 'nsmlx'), 'resnet50.pt', 'resnet101.pt', 'efficientnet_b0.pt', 'efficientnet_b1.pt']:\n",
- " for d in 'mnist', 'fashion-mnist', 'cifar10', 'cifar100', 'imagenette160', 'imagenette320', 'imagenette', 'imagewoof160', 'imagewoof320', 'imagewoof':\n",
- " !python classify/train.py --model {m} --data {d} --epochs 10 --project YOLOv5-cls --name {m}-{d}"
- ],
- "metadata": {
- "id": "UWGH7H6yakVl"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "# Classification val\n",
- "!bash data/scripts/get_imagenet.sh --val # download ImageNet val split (6.3G - 50000 images)\n",
- "!python classify/val.py --weights yolov5m-cls.pt --data ../datasets/imagenet --img 224 # validate"
- ],
- "metadata": {
- "id": "yYgOiFNHZx-1"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "# Validate on COCO test. Zip results.json and submit to eval server at https://competitions.codalab.org/competitions/20794\n",
- "!bash data/scripts/get_coco.sh --test # download COCO test-dev2017 (7G - 40000 images, test 20000)\n",
- "!python val.py --weights yolov5x.pt --data coco.yaml --img 640 --iou 0.65 --half --task test"
- ],
- "metadata": {
- "id": "aq4DPWGu0Bl1"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "VTRwsvA9u7ln"
- },
- "source": [
- "# TensorRT \n",
- "!pip install -U nvidia-tensorrt --index-url https://pypi.ngc.nvidia.com # install\n",
- "!python export.py --weights yolov5s.pt --include engine --imgsz 640 --device 0 # export\n",
- "!python detect.py --weights yolov5s.engine --imgsz 640 --device 0 # inference"
- ],
- "execution_count": null,
- "outputs": []
}
]
}
diff --git a/utils/__init__.py b/utils/__init__.py
index 46225c2208ce..5b9fcd517e03 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -4,9 +4,15 @@
"""
import contextlib
+import platform
import threading
+def emojis(str=''):
+ # Return platform-dependent emoji-safe version of string
+ return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
+
+
class TryExcept(contextlib.ContextDecorator):
# YOLOv5 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager
def __init__(self, msg=''):
@@ -17,7 +23,7 @@ def __enter__(self):
def __exit__(self, exc_type, value, traceback):
if value:
- print(f'{self.msg}{value}')
+ print(emojis(f"{self.msg}{': ' if self.msg else ''}{value}"))
return True
@@ -31,6 +37,16 @@ def wrapper(*args, **kwargs):
return wrapper
+def join_threads(verbose=False):
+ # Join all daemon threads, i.e. atexit.register(lambda: join_threads())
+ main_thread = threading.current_thread()
+ for t in threading.enumerate():
+ if t is not main_thread:
+ if verbose:
+ print(f'Joining thread {t.name}')
+ t.join()
+
+
def notebook_init(verbose=True):
# Check system software and hardware
print('Checking setup...')
@@ -38,24 +54,25 @@ def notebook_init(verbose=True):
import os
import shutil
- from utils.general import check_font, check_requirements, emojis, is_colab
+ from utils.general import check_font, check_requirements, is_colab
from utils.torch_utils import select_device # imports
- check_requirements(('psutil', 'IPython'))
check_font()
import psutil
- from IPython import display # to display images and clear console output
if is_colab():
shutil.rmtree('/content/sample_data', ignore_errors=True) # remove colab /sample_data directory
# System info
+ display = None
if verbose:
gb = 1 << 30 # bytes to GiB (1024 ** 3)
ram = psutil.virtual_memory().total
- total, used, free = shutil.disk_usage("/")
- display.clear_output()
+ total, used, free = shutil.disk_usage('/')
+ with contextlib.suppress(Exception): # clear display if ipython is installed
+ from IPython import display
+ display.clear_output()
s = f'({os.cpu_count()} CPUs, {ram / gb:.1f} GB RAM, {(total - free) / gb:.1f}/{total / gb:.1f} GB disk)'
else:
s = ''
diff --git a/utils/augmentations.py b/utils/augmentations.py
index a5587351f75b..7ab75f17fb18 100644
--- a/utils/augmentations.py
+++ b/utils/augmentations.py
@@ -12,7 +12,7 @@
import torchvision.transforms as T
import torchvision.transforms.functional as TF
-from utils.general import LOGGER, check_version, colorstr, resample_segments, segment2box
+from utils.general import LOGGER, check_version, colorstr, resample_segments, segment2box, xywhn2xyxy
from utils.metrics import bbox_ioa
IMAGENET_MEAN = 0.485, 0.456, 0.406 # RGB mean
@@ -21,7 +21,7 @@
class Albumentations:
# YOLOv5 Albumentations class (optional, only used if package is installed)
- def __init__(self):
+ def __init__(self, size=640):
self.transform = None
prefix = colorstr('albumentations: ')
try:
@@ -29,6 +29,7 @@ def __init__(self):
check_version(A.__version__, '1.0.3', hard=True) # version requirement
T = [
+ A.RandomResizedCrop(height=size, width=size, scale=(0.8, 1.0), ratio=(0.9, 1.11), p=0.0),
A.Blur(p=0.01),
A.MedianBlur(p=0.01),
A.ToGray(p=0.01),
@@ -200,7 +201,7 @@ def random_perspective(im,
# Transform label coordinates
n = len(targets)
if n:
- use_segments = any(x.any() for x in segments)
+ use_segments = any(x.any() for x in segments) and len(segments) == n
new = np.zeros((n, 4))
if use_segments: # warp segments
segments = resample_segments(segments) # upsample
@@ -249,12 +250,10 @@ def copy_paste(im, labels, segments, p=0.5):
if (ioa < 0.30).all(): # allow 30% obscuration of existing labels
labels = np.concatenate((labels, [[l[0], *box]]), 0)
segments.append(np.concatenate((w - s[:, 0:1], s[:, 1:2]), 1))
- cv2.drawContours(im_new, [segments[j].astype(np.int32)], -1, (255, 255, 255), cv2.FILLED)
+ cv2.drawContours(im_new, [segments[j].astype(np.int32)], -1, (1, 1, 1), cv2.FILLED)
- result = cv2.bitwise_and(src1=im, src2=im_new)
- result = cv2.flip(result, 1) # augment segments (flip left-right)
- i = result > 0 # pixels to replace
- # i[:, :] = result.max(2).reshape(h, w, 1) # act over ch
+ result = cv2.flip(im, 1) # augment segments (flip left-right)
+ i = cv2.flip(im_new, 1).astype(bool)
im[i] = result[i] # cv2.imwrite('debug.jpg', im) # debug
return im, labels, segments
@@ -281,7 +280,7 @@ def cutout(im, labels, p=0.5):
# return unobscured labels
if len(labels) and s > 0.03:
box = np.array([xmin, ymin, xmax, ymax], dtype=np.float32)
- ioa = bbox_ioa(box, labels[:, 1:5]) # intersection over area
+ ioa = bbox_ioa(box, xywhn2xyxy(labels[:, 1:5], w, h)) # intersection over area
labels = labels[ioa < 0.60] # remove >60% obscured labels
return labels
@@ -303,15 +302,17 @@ def box_candidates(box1, box2, wh_thr=2, ar_thr=100, area_thr=0.1, eps=1e-16):
return (w2 > wh_thr) & (h2 > wh_thr) & (w2 * h2 / (w1 * h1 + eps) > area_thr) & (ar < ar_thr) # candidates
-def classify_albumentations(augment=True,
- size=224,
- scale=(0.08, 1.0),
- hflip=0.5,
- vflip=0.0,
- jitter=0.4,
- mean=IMAGENET_MEAN,
- std=IMAGENET_STD,
- auto_aug=False):
+def classify_albumentations(
+ augment=True,
+ size=224,
+ scale=(0.08, 1.0),
+ ratio=(0.75, 1.0 / 0.75), # 0.75, 1.33
+ hflip=0.5,
+ vflip=0.0,
+ jitter=0.4,
+ mean=IMAGENET_MEAN,
+ std=IMAGENET_STD,
+ auto_aug=False):
# YOLOv5 classification Albumentations (optional, only used if package is installed)
prefix = colorstr('albumentations: ')
try:
@@ -319,7 +320,7 @@ def classify_albumentations(augment=True,
from albumentations.pytorch import ToTensorV2
check_version(A.__version__, '1.0.3', hard=True) # version requirement
if augment: # Resize and crop
- T = [A.RandomResizedCrop(height=size, width=size, scale=scale)]
+ T = [A.RandomResizedCrop(height=size, width=size, scale=scale, ratio=ratio)]
if auto_aug:
# TODO: implement AugMix, AutoAug & RandAug in albumentation
LOGGER.info(f'{prefix}auto augmentations are currently not supported')
@@ -338,7 +339,7 @@ def classify_albumentations(augment=True,
return A.Compose(T)
except ImportError: # package not installed, skip
- pass
+ LOGGER.warning(f'{prefix}⚠️ not found, install with `pip install albumentations` (recommended)')
except Exception as e:
LOGGER.info(f'{prefix}{e}')
diff --git a/utils/autoanchor.py b/utils/autoanchor.py
index 0b49ab3319c0..bb5cf6e6965e 100644
--- a/utils/autoanchor.py
+++ b/utils/autoanchor.py
@@ -11,7 +11,7 @@
from tqdm import tqdm
from utils import TryExcept
-from utils.general import LOGGER, colorstr
+from utils.general import LOGGER, TQDM_BAR_FORMAT, colorstr
PREFIX = colorstr('AutoAnchor: ')
@@ -26,7 +26,7 @@ def check_anchor_order(m):
m.anchors[:] = m.anchors.flip(0)
-@TryExcept(f'{PREFIX}ERROR: ')
+@TryExcept(f'{PREFIX}ERROR')
def check_anchors(dataset, model, thr=4.0, imgsz=640):
# Check anchor fit to data, recompute if necessary
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
@@ -122,7 +122,7 @@ def print_results(k, verbose=True):
# Filter
i = (wh0 < 3.0).any(1).sum()
if i:
- LOGGER.info(f'{PREFIX}WARNING: Extremely small objects found: {i} of {len(wh0)} labels are < 3 pixels in size')
+ LOGGER.info(f'{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size')
wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32) # filter > 2 pixels
# wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
@@ -134,7 +134,7 @@ def print_results(k, verbose=True):
k = kmeans(wh / s, n, iter=30)[0] * s # points
assert n == len(k) # kmeans may return fewer points than requested if wh is insufficient or too similar
except Exception:
- LOGGER.warning(f'{PREFIX}WARNING: switching strategies from kmeans to random init')
+ LOGGER.warning(f'{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init')
k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size # random init
wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))
k = print_results(k, verbose=False)
@@ -153,7 +153,7 @@ def print_results(k, verbose=True):
# Evolve
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
- pbar = tqdm(range(gen), bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
+ pbar = tqdm(range(gen), bar_format=TQDM_BAR_FORMAT) # progress bar
for _ in pbar:
v = np.ones(sh)
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
diff --git a/utils/autobatch.py b/utils/autobatch.py
index 641b055b9fe3..bdeb91c3d2bd 100644
--- a/utils/autobatch.py
+++ b/utils/autobatch.py
@@ -19,7 +19,7 @@ def check_train_batch_size(model, imgsz=640, amp=True):
def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
- # Automatically estimate best batch size to use `fraction` of available CUDA memory
+ # Automatically estimate best YOLOv5 batch size to use `fraction` of available CUDA memory
# Usage:
# import torch
# from utils.autobatch import autobatch
@@ -33,6 +33,9 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
if device.type == 'cpu':
LOGGER.info(f'{prefix}CUDA not detected, using default CPU batch-size {batch_size}')
return batch_size
+ if torch.backends.cudnn.benchmark:
+ LOGGER.info(f'{prefix} ⚠️ Requires torch.backends.cudnn.benchmark=False, using default batch-size {batch_size}')
+ return batch_size
# Inspect CUDA memory
gb = 1 << 30 # bytes to GiB (1024 ** 3)
@@ -62,8 +65,8 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
b = batch_sizes[max(i - 1, 0)] # select prior safe point
if b < 1 or b > 1024: # b outside of safe range
b = batch_size
- LOGGER.warning(f'{prefix}WARNING: ⚠️ CUDA anomaly detected, recommend restart environment and retry command.')
+ LOGGER.warning(f'{prefix}WARNING ⚠️ CUDA anomaly detected, recommend restart environment and retry command.')
- fraction = np.polyval(p, b) / t # actual fraction predicted
+ fraction = (np.polyval(p, b) + r + a) / t # actual fraction predicted
LOGGER.info(f'{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%) ✅')
return b
diff --git a/utils/dataloaders.py b/utils/dataloaders.py
old mode 100755
new mode 100644
index d8ef11fd94b4..7687a2ba2665
--- a/utils/dataloaders.py
+++ b/utils/dataloaders.py
@@ -17,9 +17,9 @@
from pathlib import Path
from threading import Thread
from urllib.parse import urlparse
-from zipfile import ZipFile
import numpy as np
+import psutil
import torch
import torch.nn.functional as F
import torchvision
@@ -30,16 +30,17 @@
from utils.augmentations import (Albumentations, augment_hsv, classify_albumentations, classify_transforms, copy_paste,
letterbox, mixup, random_perspective)
-from utils.general import (DATASETS_DIR, LOGGER, NUM_THREADS, check_dataset, check_requirements, check_yaml, clean_str,
- cv2, is_colab, is_kaggle, segments2boxes, xyn2xy, xywh2xyxy, xywhn2xyxy, xyxy2xywhn)
+from utils.general import (DATASETS_DIR, LOGGER, NUM_THREADS, TQDM_BAR_FORMAT, check_dataset, check_requirements,
+ check_yaml, clean_str, cv2, is_colab, is_kaggle, segments2boxes, unzip_file, xyn2xy,
+ xywh2xyxy, xywhn2xyxy, xyxy2xywhn)
from utils.torch_utils import torch_distributed_zero_first
# Parameters
HELP_URL = 'See https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data'
IMG_FORMATS = 'bmp', 'dng', 'jpeg', 'jpg', 'mpo', 'png', 'tif', 'tiff', 'webp', 'pfm' # include image suffixes
VID_FORMATS = 'asf', 'avi', 'gif', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'ts', 'wmv' # include video suffixes
-BAR_FORMAT = '{l_bar}{bar:10}{r_bar}{bar:-10b}' # tqdm bar format
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
+RANK = int(os.getenv('RANK', -1))
PIN_MEMORY = str(os.getenv('PIN_MEMORY', True)).lower() == 'true' # global pin_memory for dataloaders
# Get orientation exif tag
@@ -51,7 +52,7 @@
def get_hash(paths):
# Returns a single hash value of a list of paths (files or dirs)
size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes
- h = hashlib.md5(str(size).encode()) # hash sizes
+ h = hashlib.sha256(str(size).encode()) # hash sizes
h.update(''.join(paths).encode()) # hash paths
return h.hexdigest() # return hash
@@ -88,7 +89,7 @@ def exif_transpose(image):
if method is not None:
image = image.transpose(method)
del exif[0x0112]
- image.info["exif"] = exif.tobytes()
+ image.info['exif'] = exif.tobytes()
return image
@@ -114,9 +115,10 @@ def create_dataloader(path,
image_weights=False,
quad=False,
prefix='',
- shuffle=False):
+ shuffle=False,
+ seed=0):
if rect and shuffle:
- LOGGER.warning('WARNING: --rect is incompatible with DataLoader shuffle, setting shuffle=False')
+ LOGGER.warning('WARNING ⚠️ --rect is incompatible with DataLoader shuffle, setting shuffle=False')
shuffle = False
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
dataset = LoadImagesAndLabels(
@@ -139,7 +141,7 @@ def create_dataloader(path,
sampler = None if rank == -1 else distributed.DistributedSampler(dataset, shuffle=shuffle)
loader = DataLoader if image_weights else InfiniteDataLoader # only DataLoader allows for attribute updates
generator = torch.Generator()
- generator.manual_seed(0)
+ generator.manual_seed(6148914691236517205 + seed + RANK)
return loader(dataset,
batch_size=batch_size,
shuffle=shuffle and sampler is None,
@@ -185,9 +187,60 @@ def __iter__(self):
yield from iter(self.sampler)
+class LoadScreenshots:
+ # YOLOv5 screenshot dataloader, i.e. `python detect.py --source "screen 0 100 100 512 256"`
+ def __init__(self, source, img_size=640, stride=32, auto=True, transforms=None):
+ # source = [screen_number left top width height] (pixels)
+ check_requirements('mss')
+ import mss
+
+ source, *params = source.split()
+ self.screen, left, top, width, height = 0, None, None, None, None # default to full screen 0
+ if len(params) == 1:
+ self.screen = int(params[0])
+ elif len(params) == 4:
+ left, top, width, height = (int(x) for x in params)
+ elif len(params) == 5:
+ self.screen, left, top, width, height = (int(x) for x in params)
+ self.img_size = img_size
+ self.stride = stride
+ self.transforms = transforms
+ self.auto = auto
+ self.mode = 'stream'
+ self.frame = 0
+ self.sct = mss.mss()
+
+ # Parse monitor shape
+ monitor = self.sct.monitors[self.screen]
+ self.top = monitor['top'] if top is None else (monitor['top'] + top)
+ self.left = monitor['left'] if left is None else (monitor['left'] + left)
+ self.width = width or monitor['width']
+ self.height = height or monitor['height']
+ self.monitor = {'left': self.left, 'top': self.top, 'width': self.width, 'height': self.height}
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ # mss screen capture: get raw pixels from the screen as np array
+ im0 = np.array(self.sct.grab(self.monitor))[:, :, :3] # [:, :, :3] BGRA to BGR
+ s = f'screen {self.screen} (LTWH): {self.left},{self.top},{self.width},{self.height}: '
+
+ if self.transforms:
+ im = self.transforms(im0) # transforms
+ else:
+ im = letterbox(im0, self.img_size, stride=self.stride, auto=self.auto)[0] # padded resize
+ im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
+ im = np.ascontiguousarray(im) # contiguous
+ self.frame += 1
+ return str(self.screen), im, im0, None, s # screen, img, original img, im0s, s
+
+
class LoadImages:
# YOLOv5 image/video dataloader, i.e. `python detect.py --source image.jpg/vid.mp4`
def __init__(self, path, img_size=640, stride=32, auto=True, transforms=None, vid_stride=1):
+ if isinstance(path, str) and Path(path).suffix == '.txt': # *.txt file with img/vid/dir on each line
+ path = Path(path).read_text().rsplit()
files = []
for p in sorted(path) if isinstance(path, (list, tuple)) else [path]:
p = str(Path(p).resolve())
@@ -232,8 +285,9 @@ def __next__(self):
if self.video_flag[self.count]:
# Read video
self.mode = 'video'
- ret_val, im0 = self.cap.read()
- self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.vid_stride * (self.frame + 1)) # read at vid_stride
+ for _ in range(self.vid_stride):
+ self.cap.grab()
+ ret_val, im0 = self.cap.retrieve()
while not ret_val:
self.count += 1
self.cap.release()
@@ -287,13 +341,13 @@ def __len__(self):
class LoadStreams:
# YOLOv5 streamloader, i.e. `python detect.py --source 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP streams`
- def __init__(self, sources='streams.txt', img_size=640, stride=32, auto=True, transforms=None, vid_stride=1):
+ def __init__(self, sources='file.streams', img_size=640, stride=32, auto=True, transforms=None, vid_stride=1):
torch.backends.cudnn.benchmark = True # faster for fixed-size inference
self.mode = 'stream'
self.img_size = img_size
self.stride = stride
self.vid_stride = vid_stride # video frame-rate stride
- sources = Path(sources).read_text().rsplit() if Path(sources).is_file() else [sources]
+ sources = Path(sources).read_text().rsplit() if os.path.isfile(sources) else [sources]
n = len(sources)
self.sources = [clean_str(x) for x in sources] # clean source names for later
self.imgs, self.fps, self.frames, self.threads = [None] * n, [0] * n, [0] * n, [None] * n
@@ -301,9 +355,10 @@ def __init__(self, sources='streams.txt', img_size=640, stride=32, auto=True, tr
# Start thread to read frames from video stream
st = f'{i + 1}/{n}: {s}... '
if urlparse(s).hostname in ('www.youtube.com', 'youtube.com', 'youtu.be'): # if source is YouTube video
+ # YouTube format i.e. 'https://www.youtube.com/watch?v=Zgi9g1ksQHc' or 'https://youtu.be/Zgi9g1ksQHc'
check_requirements(('pafy', 'youtube_dl==2020.12.2'))
import pafy
- s = pafy.new(s).getbest(preftype="mp4").url # YouTube URL
+ s = pafy.new(s).getbest(preftype='mp4').url # YouTube URL
s = eval(s) if s.isnumeric() else s # i.e. s = '0' local webcam
if s == 0:
assert not is_colab(), '--source 0 webcam unsupported on Colab. Rerun command in a local environment.'
@@ -318,7 +373,7 @@ def __init__(self, sources='streams.txt', img_size=640, stride=32, auto=True, tr
_, self.imgs[i] = cap.read() # guarantee first frame
self.threads[i] = Thread(target=self.update, args=([i, cap, s]), daemon=True)
- LOGGER.info(f"{st} Success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)")
+ LOGGER.info(f'{st} Success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)')
self.threads[i].start()
LOGGER.info('') # newline
@@ -328,7 +383,7 @@ def __init__(self, sources='streams.txt', img_size=640, stride=32, auto=True, tr
self.auto = auto and self.rect
self.transforms = transforms # optional
if not self.rect:
- LOGGER.warning('WARNING: Stream shapes differ. For optimal performance supply similarly-shaped streams.')
+ LOGGER.warning('WARNING ⚠️ Stream shapes differ. For optimal performance supply similarly-shaped streams.')
def update(self, i, cap, stream):
# Read stream `i` frames in daemon thread
@@ -341,7 +396,7 @@ def update(self, i, cap, stream):
if success:
self.imgs[i] = im
else:
- LOGGER.warning('WARNING: Video stream unresponsive, please check your IP camera connection.')
+ LOGGER.warning('WARNING ⚠️ Video stream unresponsive, please check your IP camera connection.')
self.imgs[i] = np.zeros_like(self.imgs[i])
cap.open(stream) # re-open stream if signal was lost
time.sleep(0.0) # wait time
@@ -393,6 +448,7 @@ def __init__(self,
single_cls=False,
stride=32,
pad=0.0,
+ min_items=0,
prefix=''):
self.img_size = img_size
self.augment = augment
@@ -403,7 +459,7 @@ def __init__(self,
self.mosaic_border = [-img_size // 2, -img_size // 2]
self.stride = stride
self.path = path
- self.albumentations = Albumentations() if augment else None
+ self.albumentations = Albumentations(size=img_size) if augment else None
try:
f = [] # image files
@@ -416,15 +472,15 @@ def __init__(self,
with open(p) as t:
t = t.read().strip().splitlines()
parent = str(p.parent) + os.sep
- f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path
- # f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
+ f += [x.replace('./', parent, 1) if x.startswith('./') else x for x in t] # to global path
+ # f += [p.parent / x.lstrip(os.sep) for x in t] # to global path (pathlib)
else:
raise FileNotFoundError(f'{prefix}{p} does not exist')
self.im_files = sorted(x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS)
# self.img_files = sorted([x for x in f if x.suffix[1:].lower() in IMG_FORMATS]) # pathlib
assert self.im_files, f'{prefix}No images found'
except Exception as e:
- raise Exception(f'{prefix}Error loading data from {path}: {e}\n{HELP_URL}')
+ raise Exception(f'{prefix}Error loading data from {path}: {e}\n{HELP_URL}') from e
# Check cache
self.label_files = img2label_paths(self.im_files) # labels
@@ -439,8 +495,8 @@ def __init__(self,
# Display cache
nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupt, total
if exists and LOCAL_RANK in {-1, 0}:
- d = f"Scanning '{cache_path}' images and labels... {nf} found, {nm} missing, {ne} empty, {nc} corrupt"
- tqdm(None, desc=prefix + d, total=n, initial=n, bar_format=BAR_FORMAT) # display cache results
+ d = f'Scanning {cache_path}... {nf} images, {nm + ne} backgrounds, {nc} corrupt'
+ tqdm(None, desc=prefix + d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT) # display cache results
if cache['msgs']:
LOGGER.info('\n'.join(cache['msgs'])) # display warnings
assert nf > 0 or not augment, f'{prefix}No labels found in {cache_path}, can not start training. {HELP_URL}'
@@ -454,7 +510,19 @@ def __init__(self,
self.shapes = np.array(shapes)
self.im_files = list(cache.keys()) # update
self.label_files = img2label_paths(cache.keys()) # update
- n = len(shapes) # number of images
+
+ # Filter images
+ if min_items:
+ include = np.array([len(x) >= min_items for x in self.labels]).nonzero()[0].astype(int)
+ LOGGER.info(f'{prefix}{n - len(include)}/{n} images filtered from dataset')
+ self.im_files = [self.im_files[i] for i in include]
+ self.label_files = [self.label_files[i] for i in include]
+ self.labels = [self.labels[i] for i in include]
+ self.segments = [self.segments[i] for i in include]
+ self.shapes = self.shapes[include] # wh
+
+ # Create indices
+ n = len(self.shapes) # number of images
bi = np.floor(np.arange(n) / batch_size).astype(int) # batch index
nb = bi[-1] + 1 # number of batches
self.batch = bi # batch index of image
@@ -472,8 +540,6 @@ def __init__(self,
self.segments[i] = segment[j]
if single_cls: # single-class training, merge all classes into 0
self.labels[i][:, 0] = 0
- if segment:
- self.segments[i][:, 0] = 0
# Rectangular Training
if self.rect:
@@ -484,6 +550,7 @@ def __init__(self,
self.im_files = [self.im_files[i] for i in irect]
self.label_files = [self.label_files[i] for i in irect]
self.labels = [self.labels[i] for i in irect]
+ self.segments = [self.segments[i] for i in irect]
self.shapes = s[irect] # wh
ar = ar[irect]
@@ -499,34 +566,53 @@ def __init__(self,
self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(int) * stride
- # Cache images into RAM/disk for faster training (WARNING: large datasets may exceed system resources)
+ # Cache images into RAM/disk for faster training
+ if cache_images == 'ram' and not self.check_cache_ram(prefix=prefix):
+ cache_images = False
self.ims = [None] * n
self.npy_files = [Path(f).with_suffix('.npy') for f in self.im_files]
if cache_images:
- gb = 0 # Gigabytes of cached images
+ b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
self.im_hw0, self.im_hw = [None] * n, [None] * n
fcn = self.cache_images_to_disk if cache_images == 'disk' else self.load_image
results = ThreadPool(NUM_THREADS).imap(fcn, range(n))
- pbar = tqdm(enumerate(results), total=n, bar_format=BAR_FORMAT, disable=LOCAL_RANK > 0)
+ pbar = tqdm(enumerate(results), total=n, bar_format=TQDM_BAR_FORMAT, disable=LOCAL_RANK > 0)
for i, x in pbar:
if cache_images == 'disk':
- gb += self.npy_files[i].stat().st_size
+ b += self.npy_files[i].stat().st_size
else: # 'ram'
self.ims[i], self.im_hw0[i], self.im_hw[i] = x # im, hw_orig, hw_resized = load_image(self, i)
- gb += self.ims[i].nbytes
- pbar.desc = f'{prefix}Caching images ({gb / 1E9:.1f}GB {cache_images})'
+ b += self.ims[i].nbytes
+ pbar.desc = f'{prefix}Caching images ({b / gb:.1f}GB {cache_images})'
pbar.close()
+ def check_cache_ram(self, safety_margin=0.1, prefix=''):
+ # Check image caching requirements vs available memory
+ b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
+ n = min(self.n, 30) # extrapolate from 30 random images
+ for _ in range(n):
+ im = cv2.imread(random.choice(self.im_files)) # sample image
+ ratio = self.img_size / max(im.shape[0], im.shape[1]) # max(h, w) # ratio
+ b += im.nbytes * ratio ** 2
+ mem_required = b * self.n / n # GB required to cache dataset into RAM
+ mem = psutil.virtual_memory()
+ cache = mem_required * (1 + safety_margin) < mem.available # to cache or not to cache, that is the question
+ if not cache:
+ LOGGER.info(f'{prefix}{mem_required / gb:.1f}GB RAM required, '
+ f'{mem.available / gb:.1f}/{mem.total / gb:.1f}GB available, '
+ f"{'caching images ✅' if cache else 'not caching images ⚠️'}")
+ return cache
+
def cache_labels(self, path=Path('./labels.cache'), prefix=''):
# Cache dataset labels, check images and read shapes
x = {} # dict
nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages
- desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..."
+ desc = f'{prefix}Scanning {path.parent / path.stem}...'
with Pool(NUM_THREADS) as pool:
pbar = tqdm(pool.imap(verify_image_label, zip(self.im_files, self.label_files, repeat(prefix))),
desc=desc,
total=len(self.im_files),
- bar_format=BAR_FORMAT)
+ bar_format=TQDM_BAR_FORMAT)
for im_file, lb, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
nm += nm_f
nf += nf_f
@@ -536,13 +622,13 @@ def cache_labels(self, path=Path('./labels.cache'), prefix=''):
x[im_file] = [lb, shape, segments]
if msg:
msgs.append(msg)
- pbar.desc = f"{desc}{nf} found, {nm} missing, {ne} empty, {nc} corrupt"
+ pbar.desc = f'{desc} {nf} images, {nm + ne} backgrounds, {nc} corrupt'
pbar.close()
if msgs:
LOGGER.info('\n'.join(msgs))
if nf == 0:
- LOGGER.warning(f'{prefix}WARNING: No labels found in {path}. {HELP_URL}')
+ LOGGER.warning(f'{prefix}WARNING ⚠️ No labels found in {path}. {HELP_URL}')
x['hash'] = get_hash(self.label_files + self.im_files)
x['results'] = nf, nm, ne, nc, len(self.im_files)
x['msgs'] = msgs # warnings
@@ -552,7 +638,7 @@ def cache_labels(self, path=Path('./labels.cache'), prefix=''):
path.with_suffix('.cache.npy').rename(path) # remove .npy suffix
LOGGER.info(f'{prefix}New cache created: {path}')
except Exception as e:
- LOGGER.warning(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # not writeable
+ LOGGER.warning(f'{prefix}WARNING ⚠️ Cache directory {path.parent} is not writeable: {e}') # not writeable
return x
def __len__(self):
@@ -651,7 +737,7 @@ def load_image(self, i):
r = self.img_size / max(h0, w0) # ratio
if r != 1: # if sizes are not equal
interp = cv2.INTER_LINEAR if (self.augment or r > 1) else cv2.INTER_AREA
- im = cv2.resize(im, (int(w0 * r), int(h0 * r)), interpolation=interp)
+ im = cv2.resize(im, (math.ceil(w0 * r), math.ceil(h0 * r)), interpolation=interp)
return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized
return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized
@@ -783,6 +869,7 @@ def load_mosaic9(self, index):
# img9, labels9 = replicate(img9, labels9) # replicate
# Augment
+ img9, labels9, segments9 = copy_paste(img9, labels9, segments9, p=self.hyp['copy_paste'])
img9, labels9 = random_perspective(img9,
labels9,
segments9,
@@ -916,7 +1003,7 @@ def verify_image_label(args):
f.seek(-2, 2)
if f.read() != b'\xff\xd9': # corrupt JPEG
ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100)
- msg = f'{prefix}WARNING: {im_file}: corrupt JPEG restored and saved'
+ msg = f'{prefix}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved'
# verify labels
if os.path.isfile(lb_file):
@@ -938,7 +1025,7 @@ def verify_image_label(args):
lb = lb[i] # remove duplicates
if segments:
segments = [segments[x] for x in i]
- msg = f'{prefix}WARNING: {im_file}: {nl - len(i)} duplicate labels removed'
+ msg = f'{prefix}WARNING ⚠️ {im_file}: {nl - len(i)} duplicate labels removed'
else:
ne = 1 # label empty
lb = np.zeros((0, 5), dtype=np.float32)
@@ -948,18 +1035,23 @@ def verify_image_label(args):
return im_file, lb, shape, segments, nm, nf, ne, nc, msg
except Exception as e:
nc = 1
- msg = f'{prefix}WARNING: {im_file}: ignoring corrupt image/label: {e}'
+ msg = f'{prefix}WARNING ⚠️ {im_file}: ignoring corrupt image/label: {e}'
return [None, None, None, None, nm, nf, ne, nc, msg]
class HUBDatasetStats():
- """ Return dataset statistics dictionary with images and instances counts per split per class
- To run in parent directory: export PYTHONPATH="$PWD/yolov5"
- Usage1: from utils.dataloaders import *; HUBDatasetStats('coco128.yaml', autodownload=True)
- Usage2: from utils.dataloaders import *; HUBDatasetStats('path/to/coco128_with_yaml.zip')
+ """ Class for generating HUB dataset JSON and `-hub` dataset directory
+
Arguments
path: Path to data.yaml or data.zip (with data.yaml inside data.zip)
autodownload: Attempt to download dataset if not found locally
+
+ Usage
+ from utils.dataloaders import HUBDatasetStats
+ stats = HUBDatasetStats('coco128.yaml', autodownload=True) # usage 1
+ stats = HUBDatasetStats('path/to/coco128.zip') # usage 2
+ stats.get_json(save=False)
+ stats.process_images()
"""
def __init__(self, path='coco128.yaml', autodownload=False):
@@ -971,7 +1063,7 @@ def __init__(self, path='coco128.yaml', autodownload=False):
if zipped:
data['path'] = data_dir
except Exception as e:
- raise Exception("error/HUB/dataset_stats/yaml_load") from e
+ raise Exception('error/HUB/dataset_stats/yaml_load') from e
check_dataset(data, autodownload) # download dataset if missing
self.hub_dir = Path(data['path'] + '-hub')
@@ -996,7 +1088,7 @@ def _unzip(self, path):
if not str(path).endswith('.zip'): # path is data.yaml
return False, None, path
assert Path(path).is_file(), f'Error unzipping {path}, file not found'
- ZipFile(path).extractall(path=path.parent) # unzip
+ unzip_file(path, path=path.parent)
dir = path.with_suffix('') # dataset directory == zip name
assert dir.is_dir(), f'Error unzipping {path}, {dir} not found. path/to/abc.zip MUST unzip to path/to/abc/'
return True, str(dir), self._find_yaml(dir) # zipped, data_dir, yaml_path
@@ -1011,7 +1103,7 @@ def _hub_ops(self, f, max_dim=1920):
im = im.resize((int(im.width * r), int(im.height * r)))
im.save(f_new, 'JPEG', quality=50, optimize=True) # save
except Exception as e: # use OpenCV
- print(f'WARNING: HUB ops PIL failure {f}: {e}')
+ LOGGER.info(f'WARNING ⚠️ HUB ops PIL failure {f}: {e}')
im = cv2.imread(f)
im_height, im_width = im.shape[:2]
r = max_dim / max(im_height, im_width) # ratio
@@ -1096,7 +1188,7 @@ def __getitem__(self, i):
else: # read image
im = cv2.imread(f) # BGR
if self.album_transforms:
- sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))["image"]
+ sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))['image']
else:
sample = self.torch_transforms(im)
return sample, j
@@ -1118,7 +1210,7 @@ def create_classification_dataloader(path,
nw = min([os.cpu_count() // max(nd, 1), batch_size if batch_size > 1 else 0, workers])
sampler = None if rank == -1 else distributed.DistributedSampler(dataset, shuffle=shuffle)
generator = torch.Generator()
- generator.manual_seed(0)
+ generator.manual_seed(6148914691236517205 + RANK)
return InfiniteDataLoader(dataset,
batch_size=batch_size,
shuffle=shuffle and sampler is None,
diff --git a/utils/docker/Dockerfile b/utils/docker/Dockerfile
index 4b9367cc27db..b5d2af9fb08e 100644
--- a/utils/docker/Dockerfile
+++ b/utils/docker/Dockerfile
@@ -3,33 +3,43 @@
# Image is CUDA-optimized for YOLOv5 single/multi-GPU training and inference
# Start FROM NVIDIA PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch
-FROM nvcr.io/nvidia/pytorch:22.07-py3
-RUN rm -rf /opt/pytorch # remove 1.2GB dir
+# FROM docker.io/pytorch/pytorch:latest
+FROM pytorch/pytorch:latest
# Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
# Install linux packages
-RUN apt update && apt install --no-install-recommends -y zip htop screen libgl1-mesa-glx
+ENV DEBIAN_FRONTEND noninteractive
+RUN apt update
+RUN TZ=Etc/UTC apt install -y tzdata
+RUN apt install --no-install-recommends -y gcc git zip curl htop libgl1-mesa-glx libglib2.0-0 libpython3-dev gnupg
+# RUN alias python=python3
-# Install pip packages
-COPY requirements.txt .
-RUN python -m pip install --upgrade pip wheel
-RUN pip uninstall -y Pillow torchtext torch torchvision
-RUN pip install --no-cache -r requirements.txt albumentations wandb gsutil notebook Pillow>=9.1.0 \
- 'opencv-python<4.6.0.66' \
- --extra-index-url https://download.pytorch.org/whl/cu113
+# Security updates
+# https://security.snyk.io/vuln/SNYK-UBUNTU1804-OPENSSL-3314796
+RUN apt upgrade --no-install-recommends -y openssl
# Create working directory
-RUN mkdir -p /usr/src/app
+RUN rm -rf /usr/src/app && mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Copy contents
# COPY . /usr/src/app (issues as not a .git directory)
RUN git clone https://github.com/ultralytics/yolov5 /usr/src/app
+# Install pip packages
+COPY requirements.txt .
+RUN python3 -m pip install --upgrade pip wheel
+RUN pip install --no-cache -r requirements.txt albumentations comet gsutil notebook \
+ coremltools onnx onnx-simplifier onnxruntime 'openvino-dev>=2022.3'
+ # tensorflow tensorflowjs \
+
# Set environment variables
-ENV OMP_NUM_THREADS=8
+ENV OMP_NUM_THREADS=1
+
+# Cleanup
+ENV DEBIAN_FRONTEND teletype
# Usage Examples -------------------------------------------------------------------------------------------------------
@@ -53,7 +63,7 @@ ENV OMP_NUM_THREADS=8
# t=ultralytics/yolov5:latest tnew=ultralytics/yolov5:v6.2 && sudo docker pull $t && sudo docker tag $t $tnew && sudo docker push $tnew
# Clean up
-# docker system prune -a --volumes
+# sudo docker system prune -a --volumes
# Update Ubuntu drivers
# https://www.maketecheasier.com/install-nvidia-drivers-ubuntu/
diff --git a/utils/docker/Dockerfile-arm64 b/utils/docker/Dockerfile-arm64
index 6e8ff77545c5..7023c6a4bb1f 100644
--- a/utils/docker/Dockerfile-arm64
+++ b/utils/docker/Dockerfile-arm64
@@ -3,25 +3,24 @@
# Image is aarch64-compatible for Apple M1 and other ARM architectures i.e. Jetson Nano and Raspberry Pi
# Start FROM Ubuntu image https://hub.docker.com/_/ubuntu
-FROM arm64v8/ubuntu:20.04
+FROM arm64v8/ubuntu:rolling
# Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
# Install linux packages
+ENV DEBIAN_FRONTEND noninteractive
RUN apt update
-RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y tzdata
+RUN TZ=Etc/UTC apt install -y tzdata
RUN apt install --no-install-recommends -y python3-pip git zip curl htop gcc libgl1-mesa-glx libglib2.0-0 libpython3-dev
# RUN alias python=python3
# Install pip packages
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip wheel
-RUN pip install --no-cache -r requirements.txt gsutil notebook \
- tensorflow-aarch64
- # tensorflowjs \
- # onnx onnx-simplifier onnxruntime \
- # coremltools openvino-dev \
+RUN pip install --no-cache -r requirements.txt albumentations gsutil notebook \
+ coremltools onnx onnxruntime
+ # tensorflow-aarch64 tensorflowjs \
# Create working directory
RUN mkdir -p /usr/src/app
@@ -30,12 +29,13 @@ WORKDIR /usr/src/app
# Copy contents
# COPY . /usr/src/app (issues as not a .git directory)
RUN git clone https://github.com/ultralytics/yolov5 /usr/src/app
+ENV DEBIAN_FRONTEND teletype
# Usage Examples -------------------------------------------------------------------------------------------------------
# Build and Push
-# t=ultralytics/yolov5:latest-M1 && sudo docker build --platform linux/arm64 -f utils/docker/Dockerfile-arm64 -t $t . && sudo docker push $t
+# t=ultralytics/yolov5:latest-arm64 && sudo docker build --platform linux/arm64 -f utils/docker/Dockerfile-arm64 -t $t . && sudo docker push $t
# Pull and Run
-# t=ultralytics/yolov5:latest-M1 && sudo docker pull $t && sudo docker run -it --ipc=host -v "$(pwd)"/datasets:/usr/src/datasets $t
+# t=ultralytics/yolov5:latest-arm64 && sudo docker pull $t && sudo docker run -it --ipc=host -v "$(pwd)"/datasets:/usr/src/datasets $t
diff --git a/utils/docker/Dockerfile-cpu b/utils/docker/Dockerfile-cpu
index d6fac645dba1..06bad9a3790d 100644
--- a/utils/docker/Dockerfile-cpu
+++ b/utils/docker/Dockerfile-cpu
@@ -3,23 +3,24 @@
# Image is CPU-optimized for ONNX, OpenVINO and PyTorch YOLOv5 deployments
# Start FROM Ubuntu image https://hub.docker.com/_/ubuntu
-FROM ubuntu:20.04
+FROM ubuntu:rolling
# Downloads to user config dir
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
# Install linux packages
+ENV DEBIAN_FRONTEND noninteractive
RUN apt update
-RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y tzdata
-RUN apt install --no-install-recommends -y python3-pip git zip curl htop libgl1-mesa-glx libglib2.0-0 libpython3-dev
+RUN TZ=Etc/UTC apt install -y tzdata
+RUN apt install --no-install-recommends -y python3-pip git zip curl htop libgl1-mesa-glx libglib2.0-0 libpython3-dev gnupg
# RUN alias python=python3
# Install pip packages
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip wheel
RUN pip install --no-cache -r requirements.txt albumentations gsutil notebook \
- coremltools onnx onnx-simplifier onnxruntime tensorflow-cpu tensorflowjs \
- # openvino-dev \
+ coremltools onnx onnx-simplifier onnxruntime 'openvino-dev>=2022.3' \
+ # tensorflow tensorflowjs \
--extra-index-url https://download.pytorch.org/whl/cpu
# Create working directory
@@ -29,6 +30,7 @@ WORKDIR /usr/src/app
# Copy contents
# COPY . /usr/src/app (issues as not a .git directory)
RUN git clone https://github.com/ultralytics/yolov5 /usr/src/app
+ENV DEBIAN_FRONTEND teletype
# Usage Examples -------------------------------------------------------------------------------------------------------
diff --git a/utils/downloads.py b/utils/downloads.py
index dd2698f995a4..643b529fba3b 100644
--- a/utils/downloads.py
+++ b/utils/downloads.py
@@ -5,32 +5,31 @@
import logging
import os
-import platform
import subprocess
-import time
import urllib
from pathlib import Path
-from zipfile import ZipFile
import requests
import torch
-def is_url(url, check_online=True):
- # Check if online file exists
+def is_url(url, check=True):
+ # Check if string is URL and check if URL exists
try:
url = str(url)
result = urllib.parse.urlparse(url)
- assert all([result.scheme, result.netloc, result.path]) # check if is url
- return (urllib.request.urlopen(url).getcode() == 200) if check_online else True # check if exists online
+ assert all([result.scheme, result.netloc]) # check if is url
+ return (urllib.request.urlopen(url).getcode() == 200) if check else True # check if exists online
except (AssertionError, urllib.request.HTTPError):
return False
def gsutil_getsize(url=''):
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
- s = subprocess.check_output(f'gsutil du {url}', shell=True).decode('utf-8')
- return eval(s.split(' ')[0]) if len(s) else 0 # bytes
+ output = subprocess.check_output(['gsutil', 'du', url], shell=True, encoding='utf-8')
+ if output:
+ return int(output.split()[0])
+ return 0
def url_getsize(url='https://ultralytics.com/images/bus.jpg'):
@@ -39,6 +38,25 @@ def url_getsize(url='https://ultralytics.com/images/bus.jpg'):
return int(response.headers.get('content-length', -1))
+def curl_download(url, filename, *, silent: bool = False) -> bool:
+ """
+ Download a file from a url to a filename using curl.
+ """
+ silent_option = 'sS' if silent else '' # silent
+ proc = subprocess.run([
+ 'curl',
+ '-#',
+ f'-{silent_option}L',
+ url,
+ '--output',
+ filename,
+ '--retry',
+ '9',
+ '-C',
+ '-',])
+ return proc.returncode == 0
+
+
def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''):
# Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes
from utils.general import LOGGER
@@ -53,23 +71,24 @@ def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''):
if file.exists():
file.unlink() # remove partial downloads
LOGGER.info(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...')
- os.system(f"curl -# -L '{url2 or url}' -o '{file}' --retry 3 -C -") # curl download, retry and resume on fail
+ # curl download, retry and resume on fail
+ curl_download(url2 or url, file)
finally:
if not file.exists() or file.stat().st_size < min_bytes: # check
if file.exists():
file.unlink() # remove partial downloads
- LOGGER.info(f"ERROR: {assert_msg}\n{error_msg}")
+ LOGGER.info(f'ERROR: {assert_msg}\n{error_msg}')
LOGGER.info('')
-def attempt_download(file, repo='ultralytics/yolov5', release='v6.2'):
- # Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v6.2', etc.
+def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'):
+ # Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v7.0', etc.
from utils.general import LOGGER
def github_assets(repository, version='latest'):
- # Return GitHub repo tag (i.e. 'v6.2') and assets (i.e. ['yolov5s.pt', 'yolov5m.pt', ...])
+ # Return GitHub repo tag (i.e. 'v7.0') and assets (i.e. ['yolov5s.pt', 'yolov5m.pt', ...])
if version != 'latest':
- version = f'tags/{version}' # i.e. tags/v6.2
+ version = f'tags/{version}' # i.e. tags/v7.0
response = requests.get(f'https://api.github.com/repos/{repository}/releases/{version}').json() # github api
return response['tag_name'], [x['name'] for x in response['assets']] # tag, assets
@@ -87,9 +106,7 @@ def github_assets(repository, version='latest'):
return file
# GitHub assets
- assets = [
- 'yolov5n.pt', 'yolov5s.pt', 'yolov5m.pt', 'yolov5l.pt', 'yolov5x.pt', 'yolov5n6.pt', 'yolov5s6.pt',
- 'yolov5m6.pt', 'yolov5l6.pt', 'yolov5x6.pt']
+ assets = [f'yolov5{size}{suffix}.pt' for size in 'nsmlx' for suffix in ('', '6', '-cls', '-seg')] # default
try:
tag, assets = github_assets(repo, release)
except Exception:
@@ -103,90 +120,9 @@ def github_assets(repository, version='latest'):
file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required)
if name in assets:
- url3 = 'https://drive.google.com/drive/folders/1EFQTEUeXWSFww0luse2jB9M1QNZQGwNl' # backup gdrive mirror
- safe_download(
- file,
- url=f'https://github.com/{repo}/releases/download/{tag}/{name}',
- url2=f'https://storage.googleapis.com/{repo}/{tag}/{name}', # backup url (optional)
- min_bytes=1E5,
- error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag} or {url3}')
+ safe_download(file,
+ url=f'https://github.com/{repo}/releases/download/{tag}/{name}',
+ min_bytes=1E5,
+ error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag}')
return str(file)
-
-
-def gdrive_download(id='16TiPfZj7htmTyhntwcZyEEAejOUxuT6m', file='tmp.zip'):
- # Downloads a file from Google Drive. from yolov5.utils.downloads import *; gdrive_download()
- t = time.time()
- file = Path(file)
- cookie = Path('cookie') # gdrive cookie
- print(f'Downloading https://drive.google.com/uc?export=download&id={id} as {file}... ', end='')
- if file.exists():
- file.unlink() # remove existing file
- if cookie.exists():
- cookie.unlink() # remove existing cookie
-
- # Attempt file download
- out = "NUL" if platform.system() == "Windows" else "/dev/null"
- os.system(f'curl -c ./cookie -s -L "drive.google.com/uc?export=download&id={id}" > {out}')
- if os.path.exists('cookie'): # large file
- s = f'curl -Lb ./cookie "drive.google.com/uc?export=download&confirm={get_token()}&id={id}" -o {file}'
- else: # small file
- s = f'curl -s -L -o {file} "drive.google.com/uc?export=download&id={id}"'
- r = os.system(s) # execute, capture return
- if cookie.exists():
- cookie.unlink() # remove existing cookie
-
- # Error check
- if r != 0:
- if file.exists():
- file.unlink() # remove partial
- print('Download error ') # raise Exception('Download error')
- return r
-
- # Unzip if archive
- if file.suffix == '.zip':
- print('unzipping... ', end='')
- ZipFile(file).extractall(path=file.parent) # unzip
- file.unlink() # remove zip
-
- print(f'Done ({time.time() - t:.1f}s)')
- return r
-
-
-def get_token(cookie="./cookie"):
- with open(cookie) as f:
- for line in f:
- if "download" in line:
- return line.split()[-1]
- return ""
-
-
-# Google utils: https://cloud.google.com/storage/docs/reference/libraries ----------------------------------------------
-#
-#
-# def upload_blob(bucket_name, source_file_name, destination_blob_name):
-# # Uploads a file to a bucket
-# # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python
-#
-# storage_client = storage.Client()
-# bucket = storage_client.get_bucket(bucket_name)
-# blob = bucket.blob(destination_blob_name)
-#
-# blob.upload_from_filename(source_file_name)
-#
-# print('File {} uploaded to {}.'.format(
-# source_file_name,
-# destination_blob_name))
-#
-#
-# def download_blob(bucket_name, source_blob_name, destination_file_name):
-# # Uploads a blob from a bucket
-# storage_client = storage.Client()
-# bucket = storage_client.get_bucket(bucket_name)
-# blob = bucket.blob(source_blob_name)
-#
-# blob.download_to_filename(destination_file_name)
-#
-# print('Blob {} downloaded to {}.'.format(
-# source_blob_name,
-# destination_file_name))
diff --git a/utils/flask_rest_api/example_request.py b/utils/flask_rest_api/example_request.py
index 773ad8932967..952e5dcb90fa 100644
--- a/utils/flask_rest_api/example_request.py
+++ b/utils/flask_rest_api/example_request.py
@@ -7,13 +7,13 @@
import requests
-DETECTION_URL = "http://localhost:5000/v1/object-detection/yolov5s"
-IMAGE = "zidane.jpg"
+DETECTION_URL = 'http://localhost:5000/v1/object-detection/yolov5s'
+IMAGE = 'zidane.jpg'
# Read image
-with open(IMAGE, "rb") as f:
+with open(IMAGE, 'rb') as f:
image_data = f.read()
-response = requests.post(DETECTION_URL, files={"image": image_data}).json()
+response = requests.post(DETECTION_URL, files={'image': image_data}).json()
pprint.pprint(response)
diff --git a/utils/flask_rest_api/restapi.py b/utils/flask_rest_api/restapi.py
index 8482435c861e..9258b1a68860 100644
--- a/utils/flask_rest_api/restapi.py
+++ b/utils/flask_rest_api/restapi.py
@@ -13,36 +13,36 @@
app = Flask(__name__)
models = {}
-DETECTION_URL = "/v1/object-detection/"
+DETECTION_URL = '/v1/object-detection/'
-@app.route(DETECTION_URL, methods=["POST"])
+@app.route(DETECTION_URL, methods=['POST'])
def predict(model):
- if request.method != "POST":
+ if request.method != 'POST':
return
- if request.files.get("image"):
+ if request.files.get('image'):
# Method 1
# with request.files["image"] as f:
# im = Image.open(io.BytesIO(f.read()))
# Method 2
- im_file = request.files["image"]
+ im_file = request.files['image']
im_bytes = im_file.read()
im = Image.open(io.BytesIO(im_bytes))
if model in models:
results = models[model](im, size=640) # reduce size=320 for faster inference
- return results.pandas().xyxy[0].to_json(orient="records")
+ return results.pandas().xyxy[0].to_json(orient='records')
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Flask API exposing YOLOv5 model")
- parser.add_argument("--port", default=5000, type=int, help="port number")
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Flask API exposing YOLOv5 model')
+ parser.add_argument('--port', default=5000, type=int, help='port number')
parser.add_argument('--model', nargs='+', default=['yolov5s'], help='model(s) to run, i.e. --model yolov5n yolov5s')
opt = parser.parse_args()
for m in opt.model:
- models[m] = torch.hub.load("ultralytics/yolov5", m, force_reload=True, skip_validation=True)
+ models[m] = torch.hub.load('ultralytics/yolov5', m, force_reload=True, skip_validation=True)
- app.run(host="0.0.0.0", port=opt.port) # debug=True causes Restarting with stat
+ app.run(host='0.0.0.0', port=opt.port) # debug=True causes Restarting with stat
diff --git a/utils/general.py b/utils/general.py
old mode 100755
new mode 100644
index f5fb2c93a3d5..74620460070e
--- a/utils/general.py
+++ b/utils/general.py
@@ -7,13 +7,14 @@
import glob
import inspect
import logging
+import logging.config
import math
import os
import platform
import random
import re
-import shutil
import signal
+import subprocess
import sys
import time
import urllib
@@ -23,8 +24,9 @@
from multiprocessing.pool import ThreadPool
from pathlib import Path
from subprocess import check_output
+from tarfile import is_tarfile
from typing import Optional
-from zipfile import ZipFile
+from zipfile import ZipFile, is_zipfile
import cv2
import numpy as np
@@ -34,8 +36,8 @@
import torchvision
import yaml
-from utils import TryExcept
-from utils.downloads import gsutil_getsize
+from utils import TryExcept, emojis
+from utils.downloads import curl_download, gsutil_getsize
from utils.metrics import box_iou, fitness
FILE = Path(__file__).resolve()
@@ -43,10 +45,11 @@
RANK = int(os.getenv('RANK', -1))
# Settings
-DATASETS_DIR = ROOT.parent / 'datasets' # YOLOv5 datasets directory
NUM_THREADS = min(8, max(1, os.cpu_count() - 1)) # number of YOLOv5 multiprocessing threads
+DATASETS_DIR = Path(os.getenv('YOLOv5_DATASETS_DIR', ROOT.parent / 'datasets')) # global datasets directory
AUTOINSTALL = str(os.getenv('YOLOv5_AUTOINSTALL', True)).lower() == 'true' # global auto-install mode
VERBOSE = str(os.getenv('YOLOv5_VERBOSE', True)).lower() == 'true' # global verbose mode
+TQDM_BAR_FORMAT = '{l_bar}{bar:10}{r_bar}' # tqdm bar format
FONT = 'Arial.ttf' # https://ultralytics.com/assets/Arial.ttf
torch.set_printoptions(linewidth=320, precision=5, profile='long')
@@ -70,7 +73,21 @@ def is_chinese(s='人工智能'):
def is_colab():
# Is environment a Google Colab instance?
- return 'COLAB_GPU' in os.environ
+ return 'google.colab' in sys.modules
+
+
+def is_jupyter():
+ """
+ Check if the current script is running inside a Jupyter Notebook.
+ Verified on Colab, Jupyterlab, Kaggle, Paperspace.
+
+ Returns:
+ bool: True if running inside a Jupyter Notebook, False otherwise.
+ """
+ with contextlib.suppress(Exception):
+ from IPython import get_ipython
+ return get_ipython() is not None
+ return False
def is_kaggle():
@@ -80,11 +97,11 @@ def is_kaggle():
def is_docker() -> bool:
"""Check if the process runs inside a docker container."""
- if Path("/.dockerenv").exists():
+ if Path('/.dockerenv').exists():
return True
try: # check if docker is in control groups
- with open("/proc/self/cgroup") as file:
- return any("docker" in line for line in file)
+ with open('/proc/self/cgroup') as file:
+ return any('docker' in line for line in file)
except OSError:
return False
@@ -103,23 +120,33 @@ def is_writeable(dir, test=False):
return False
-def set_logging(name=None, verbose=VERBOSE):
- # Sets level and returns logger
- if is_kaggle() or is_colab():
- for h in logging.root.handlers:
- logging.root.removeHandler(h) # remove all handlers associated with the root logger object
- 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
- log = logging.getLogger(name)
- log.setLevel(level)
- handler = logging.StreamHandler()
- handler.setFormatter(logging.Formatter("%(message)s"))
- handler.setLevel(level)
- log.addHandler(handler)
+LOGGING_NAME = 'yolov5'
-set_logging() # run before defining LOGGER
-LOGGER = logging.getLogger("yolov5") # define globally (used in train.py, val.py, detect.py, etc.)
+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
+ logging.config.dictConfig({
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ name: {
+ 'format': '%(message)s'}},
+ 'handlers': {
+ name: {
+ 'class': 'logging.StreamHandler',
+ 'formatter': name,
+ 'level': level,}},
+ 'loggers': {
+ name: {
+ 'level': level,
+ 'handlers': [name],
+ 'propagate': False,}}})
+
+
+set_logging(LOGGING_NAME) # run before defining LOGGER
+LOGGER = logging.getLogger(LOGGING_NAME) # define globally (used in train.py, val.py, detect.py, etc.)
if platform.system() == 'Windows':
for fn in LOGGER.info, LOGGER.warning:
setattr(LOGGER, fn.__name__, lambda x: fn(emojis(x))) # emoji safe logging
@@ -198,7 +225,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
def methods(instance):
# Get class/instance methods
- return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith("__")]
+ return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith('__')]
def print_args(args: Optional[dict] = None, show_file=True, show_func=False):
@@ -223,7 +250,7 @@ def init_seeds(seed=0, deterministic=False):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) # for Multi-GPU, exception safe
- torch.backends.cudnn.benchmark = True # for faster training
+ # torch.backends.cudnn.benchmark = True # AutoBatch problem https://github.com/ultralytics/yolov5/issues/9287
if deterministic and check_version(torch.__version__, '1.12.0'): # https://github.com/ultralytics/yolov5/pull/8213
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.deterministic = True
@@ -248,11 +275,6 @@ def get_latest_run(search_dir='.'):
return max(last_list, key=os.path.getctime) if last_list else ''
-def emojis(str=''):
- # Return platform-dependent emoji-safe version of string
- return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
-
-
def file_age(path=__file__):
# Return days since last file update
dt = (datetime.now() - datetime.fromtimestamp(Path(path).stat().st_mtime)) # delta
@@ -280,11 +302,16 @@ def file_size(path):
def check_online():
# Check internet connectivity
import socket
- try:
- socket.create_connection(("1.1.1.1", 443), 5) # check host accessibility
- return True
- except OSError:
- return False
+
+ def run_once():
+ # Check once
+ try:
+ socket.create_connection(('1.1.1.1', 443), 5) # check host accessibility
+ return True
+ except OSError:
+ return False
+
+ return run_once() or run_once() # check twice to increase robustness to intermittent connectivity issues
def git_describe(path=ROOT): # path must be a directory
@@ -318,12 +345,30 @@ def check_git_status(repo='ultralytics/yolov5', branch='master'):
n = int(check_output(f'git rev-list {local_branch}..{remote}/{branch} --count', shell=True)) # commits behind
if n > 0:
pull = 'git pull' if remote == 'origin' else f'git pull {remote} {branch}'
- s += f"⚠️ YOLOv5 is out of date by {n} commit{'s' * (n > 1)}. Use `{pull}` or `git clone {url}` to update."
+ s += f"⚠️ YOLOv5 is out of date by {n} commit{'s' * (n > 1)}. Use '{pull}' or 'git clone {url}' to update."
else:
s += f'up to date with {url} ✅'
LOGGER.info(s)
+@WorkingDirectory(ROOT)
+def check_git_info(path='.'):
+ # YOLOv5 git info check, return {remote, branch, commit}
+ check_requirements('gitpython')
+ import git
+ try:
+ repo = git.Repo(path)
+ remote = repo.remotes.origin.url.replace('.git', '') # i.e. 'https://github.com/ultralytics/yolov5'
+ commit = repo.head.commit.hexsha # i.e. '3134699c73af83aac2a481435550b968d5792c0d'
+ try:
+ branch = repo.active_branch.name # i.e. 'main'
+ except TypeError: # not on any branch
+ branch = None # i.e. 'detached HEAD' state
+ return {'remote': remote, 'branch': branch, 'commit': commit}
+ except git.exc.InvalidGitRepositoryError: # path is not a git dir
+ return {'remote': None, 'branch': None, 'commit': None}
+
+
def check_python(minimum='3.7.0'):
# Check current python version vs. required python version
check_version(platform.python_version(), minimum, name='Python ', hard=True)
@@ -333,7 +378,7 @@ def check_version(current='0.0.0', minimum='0.0.0', name='version ', pinned=Fals
# Check version vs. required version
current, minimum = (pkg.parse_version(x) for x in (current, minimum))
result = (current == minimum) if pinned else (current >= minimum) # bool
- s = f'WARNING: ⚠️ {name}{minimum} is required by YOLOv5, but {name}{current} is currently installed' # string
+ s = f'WARNING ⚠️ {name}{minimum} is required by YOLOv5, but {name}{current} is currently installed' # string
if hard:
assert result, emojis(s) # assert min requirements met
if verbose and not result:
@@ -348,7 +393,7 @@ def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), insta
check_python() # check python version
if isinstance(requirements, Path): # requirements.txt file
file = requirements.resolve()
- assert file.exists(), f"{prefix} {file} not found, check failed."
+ assert file.exists(), f'{prefix} {file} not found, check failed.'
with file.open() as f:
requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude]
elif isinstance(requirements, str):
@@ -366,14 +411,14 @@ def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), insta
if s and install and AUTOINSTALL: # check environment variable
LOGGER.info(f"{prefix} YOLOv5 requirement{'s' * (n > 1)} {s}not found, attempting AutoUpdate...")
try:
- assert check_online(), "AutoUpdate skipped (offline)"
+ # assert check_online(), "AutoUpdate skipped (offline)"
LOGGER.info(check_output(f'pip install {s} {cmds}', shell=True).decode())
source = file if 'file' in locals() else requirements
s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
LOGGER.info(s)
except Exception as e:
- LOGGER.warning(f'{prefix} {e}')
+ LOGGER.warning(f'{prefix} ❌ {e}')
def check_img_size(imgsz, s=32, floor=0):
@@ -384,22 +429,23 @@ def check_img_size(imgsz, s=32, floor=0):
imgsz = list(imgsz) # convert to list if tuple
new_size = [max(make_divisible(x, int(s)), floor) for x in imgsz]
if new_size != imgsz:
- LOGGER.warning(f'WARNING: --img-size {imgsz} must be multiple of max stride {s}, updating to {new_size}')
+ LOGGER.warning(f'WARNING ⚠️ --img-size {imgsz} must be multiple of max stride {s}, updating to {new_size}')
return new_size
-def check_imshow():
+def check_imshow(warn=False):
# Check if environment supports image displays
try:
- assert not is_docker(), 'cv2.imshow() is disabled in Docker environments'
- assert not is_colab(), 'cv2.imshow() is disabled in Google Colab environments'
+ assert not is_jupyter()
+ assert not is_docker()
cv2.imshow('test', np.zeros((1, 1, 3)))
cv2.waitKey(1)
cv2.destroyAllWindows()
cv2.waitKey(1)
return True
except Exception as e:
- LOGGER.warning(f'WARNING: Environment does not support cv2.imshow() or PIL Image.show() image displays\n{e}')
+ if warn:
+ LOGGER.warning(f'WARNING ⚠️ Environment does not support cv2.imshow() or PIL Image.show()\n{e}')
return False
@@ -411,7 +457,7 @@ def check_suffix(file='yolov5s.pt', suffix=('.pt',), msg=''):
for f in file if isinstance(file, (list, tuple)) else [file]:
s = Path(f).suffix.lower() # file suffix
if len(s):
- assert s in suffix, f"{msg}{f} acceptable suffix is {suffix}"
+ assert s in suffix, f'{msg}{f} acceptable suffix is {suffix}'
def check_yaml(file, suffix=('.yaml', '.yml')):
@@ -423,12 +469,12 @@ def check_file(file, suffix=''):
# Search/download file (if necessary) and return path
check_suffix(file, suffix) # optional
file = str(file) # convert to str()
- if Path(file).is_file() or not file: # exists
+ if os.path.isfile(file) or not file: # exists
return file
elif file.startswith(('http:/', 'https:/')): # download
url = file # warning: Pathlib turns :// -> :/
file = Path(urllib.parse.unquote(file).split('?')[0]).name # '%2F' to '/', split https://url.com/file.txt?auth
- if Path(file).is_file():
+ if os.path.isfile(file):
LOGGER.info(f'Found {url} locally at {file}') # file already exists
else:
LOGGER.info(f'Downloading {url} to {file}...')
@@ -462,30 +508,37 @@ def check_dataset(data, autodownload=True):
# Download (optional)
extract_dir = ''
- if isinstance(data, (str, Path)) and str(data).endswith('.zip'): # i.e. gs://bucket/dir/coco128.zip
+ if isinstance(data, (str, Path)) and (is_zipfile(data) or is_tarfile(data)):
download(data, dir=f'{DATASETS_DIR}/{Path(data).stem}', unzip=True, delete=False, curl=False, threads=1)
data = next((DATASETS_DIR / Path(data).stem).rglob('*.yaml'))
extract_dir, autodownload = data.parent, False
# Read yaml (optional)
if isinstance(data, (str, Path)):
- with open(data, errors='ignore') as f:
- data = yaml.safe_load(f) # dictionary
+ data = yaml_load(data) # dictionary
# Checks
for k in 'train', 'val', 'names':
- assert k in data, f"data.yaml '{k}:' field missing ❌"
+ assert k in data, emojis(f"data.yaml '{k}:' field missing ❌")
if isinstance(data['names'], (list, tuple)): # old array format
data['names'] = dict(enumerate(data['names'])) # convert to dict
+ assert all(isinstance(k, int) for k in data['names'].keys()), 'data.yaml names keys must be integers, i.e. 2: car'
data['nc'] = len(data['names'])
# Resolve paths
path = Path(extract_dir or data.get('path') or '') # optional 'path' default to '.'
if not path.is_absolute():
path = (ROOT / path).resolve()
+ data['path'] = path # download scripts
for k in 'train', 'val', 'test':
if data.get(k): # prepend path
- data[k] = str(path / data[k]) if isinstance(data[k], str) else [str(path / x) for x in data[k]]
+ if isinstance(data[k], str):
+ x = (path / data[k]).resolve()
+ if not x.exists() and data[k].startswith('../'):
+ x = (path / data[k][3:]).resolve()
+ data[k] = str(x)
+ else:
+ data[k] = [str((path / x).resolve()) for x in data[k]]
# Parse yaml
train, val, test, s = (data.get(x) for x in ('train', 'val', 'test', 'download'))
@@ -496,23 +549,22 @@ def check_dataset(data, autodownload=True):
if not s or not autodownload:
raise Exception('Dataset not found ❌')
t = time.time()
- root = path.parent if 'path' in data else '..' # unzip directory i.e. '../'
if s.startswith('http') and s.endswith('.zip'): # URL
f = Path(s).name # filename
LOGGER.info(f'Downloading {s} to {f}...')
torch.hub.download_url_to_file(s, f)
- Path(root).mkdir(parents=True, exist_ok=True) # create root
- ZipFile(f).extractall(path=root) # unzip
+ Path(DATASETS_DIR).mkdir(parents=True, exist_ok=True) # create root
+ unzip_file(f, path=DATASETS_DIR) # unzip
Path(f).unlink() # remove zip
r = None # success
elif s.startswith('bash '): # bash script
LOGGER.info(f'Running {s} ...')
- r = os.system(s)
+ r = subprocess.run(s, shell=True)
else: # python script
r = exec(s, {'yaml': data}) # return None
dt = f'({round(time.time() - t, 1)}s)'
- s = f"success ✅ {dt}, saved to {colorstr('bold', root)}" if r in (0, None) else f"failure {dt} ❌"
- LOGGER.info(f"Dataset download {s}")
+ s = f"success ✅ {dt}, saved to {colorstr('bold', DATASETS_DIR)}" if r in (0, None) else f'failure {dt} ❌'
+ LOGGER.info(f'Dataset download {s}')
check_font('Arial.ttf' if is_ascii(data['names']) else 'Arial.Unicode.ttf', progress=True) # download fonts
return data # dictionary
@@ -557,6 +609,16 @@ def yaml_save(file='data.yaml', data={}):
yaml.safe_dump({k: str(v) if isinstance(v, Path) else v for k, v in data.items()}, f, sort_keys=False)
+def unzip_file(file, path=None, exclude=('.DS_Store', '__MACOSX')):
+ # Unzip a *.zip file to path/, excluding files containing strings in exclude list
+ 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
+ if all(x not in f for x in exclude):
+ zipObj.extract(f, path=path)
+
+
def url2file(url):
# Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt
url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/
@@ -568,35 +630,32 @@ def download(url, dir='.', unzip=True, delete=True, curl=False, threads=1, retry
def download_one(url, dir):
# Download 1 file
success = True
- f = dir / Path(url).name # filename
- if Path(url).is_file(): # exists in current path
- Path(url).rename(f) # move to dir
- elif not f.exists():
+ if os.path.isfile(url):
+ f = Path(url) # filename
+ else: # does not exist
+ f = dir / Path(url).name
LOGGER.info(f'Downloading {url} to {f}...')
for i in range(retry + 1):
if curl:
- s = 'sS' if threads > 1 else '' # silent
- r = os.system(
- f'curl -# -{s}L "{url}" -o "{f}" --retry 9 -C -') # curl download with retry, continue
- success = r == 0
+ success = curl_download(url, f, silent=(threads > 1))
else:
torch.hub.download_url_to_file(url, f, progress=threads == 1) # torch download
success = f.is_file()
if success:
break
elif i < retry:
- LOGGER.warning(f'Download failure, retrying {i + 1}/{retry} {url}...')
+ LOGGER.warning(f'⚠️ Download failure, retrying {i + 1}/{retry} {url}...')
else:
- LOGGER.warning(f'Failed to download {url}...')
+ LOGGER.warning(f'❌ Failed to download {url}...')
- if unzip and success and f.suffix in ('.zip', '.tar', '.gz'):
+ if unzip and success and (f.suffix == '.gz' or is_zipfile(f) or is_tarfile(f)):
LOGGER.info(f'Unzipping {f}...')
- if f.suffix == '.zip':
- ZipFile(f).extractall(path=dir) # unzip
- elif f.suffix == '.tar':
- os.system(f'tar xf {f} --directory {f.parent}') # unzip
+ if is_zipfile(f):
+ unzip_file(f, dir) # unzip
+ elif is_tarfile(f):
+ subprocess.run(['tar', 'xf', f, '--directory', f.parent], check=True) # unzip
elif f.suffix == '.gz':
- os.system(f'tar xfz {f} --directory {f.parent}') # unzip
+ subprocess.run(['tar', 'xfz', f, '--directory', f.parent], check=True) # unzip
if delete:
f.unlink() # remove zip
@@ -621,7 +680,7 @@ def make_divisible(x, divisor):
def clean_str(s):
# Cleans a string by replacing special characters with underscore _
- return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
+ return re.sub(pattern='[|@#!¡·$€%&()=?¿^*;:,¨´><+]', repl='_', string=s)
def one_cycle(y1=0.0, y2=1.0, steps=100):
@@ -696,50 +755,50 @@ def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper)
def xyxy2xywh(x):
# Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
- y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
- y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
- y[:, 2] = x[:, 2] - x[:, 0] # width
- y[:, 3] = x[:, 3] - x[:, 1] # height
+ y[..., 0] = (x[..., 0] + x[..., 2]) / 2 # x center
+ y[..., 1] = (x[..., 1] + x[..., 3]) / 2 # y center
+ y[..., 2] = x[..., 2] - x[..., 0] # width
+ y[..., 3] = x[..., 3] - x[..., 1] # height
return y
def xywh2xyxy(x):
# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
- y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
- y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
- y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
- y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
+ y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x
+ y[..., 1] = x[..., 1] - x[..., 3] / 2 # top left y
+ y[..., 2] = x[..., 0] + x[..., 2] / 2 # bottom right x
+ y[..., 3] = x[..., 1] + x[..., 3] / 2 # bottom right y
return y
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
# Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
- y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x
- y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y
- y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x
- y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y
+ y[..., 0] = w * (x[..., 0] - x[..., 2] / 2) + padw # top left x
+ y[..., 1] = h * (x[..., 1] - x[..., 3] / 2) + padh # top left y
+ y[..., 2] = w * (x[..., 0] + x[..., 2] / 2) + padw # bottom right x
+ y[..., 3] = h * (x[..., 1] + x[..., 3] / 2) + padh # bottom right y
return y
def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0):
# Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] normalized where xy1=top-left, xy2=bottom-right
if clip:
- clip_coords(x, (h - eps, w - eps)) # warning: inplace clip
+ clip_boxes(x, (h - eps, w - eps)) # warning: inplace clip
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
- y[:, 0] = ((x[:, 0] + x[:, 2]) / 2) / w # x center
- y[:, 1] = ((x[:, 1] + x[:, 3]) / 2) / h # y center
- y[:, 2] = (x[:, 2] - x[:, 0]) / w # width
- y[:, 3] = (x[:, 3] - x[:, 1]) / h # height
+ y[..., 0] = ((x[..., 0] + x[..., 2]) / 2) / w # x center
+ y[..., 1] = ((x[..., 1] + x[..., 3]) / 2) / h # y center
+ y[..., 2] = (x[..., 2] - x[..., 0]) / w # width
+ y[..., 3] = (x[..., 3] - x[..., 1]) / h # height
return y
def xyn2xy(x, w=640, h=640, padw=0, padh=0):
# Convert normalized segments into pixel segments, shape (n,2)
y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
- y[:, 0] = w * x[:, 0] + padw # top left x
- y[:, 1] = h * x[:, 1] + padh # top left y
+ y[..., 0] = w * x[..., 0] + padw # top left x
+ y[..., 1] = h * x[..., 1] + padh # top left y
return y
@@ -770,7 +829,23 @@ def resample_segments(segments, n=1000):
return segments
-def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
+def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None):
+ # Rescale boxes (xyxy) from img1_shape to img0_shape
+ if ratio_pad is None: # calculate from img0_shape
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
+ pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
+ else:
+ gain = ratio_pad[0][0]
+ pad = ratio_pad[1]
+
+ boxes[..., [0, 2]] -= pad[0] # x padding
+ boxes[..., [1, 3]] -= pad[1] # y padding
+ boxes[..., :4] /= gain
+ clip_boxes(boxes, img0_shape)
+ return boxes
+
+
+def scale_segments(img1_shape, segments, img0_shape, ratio_pad=None, normalize=False):
# Rescale coords (xyxy) from img1_shape to img0_shape
if ratio_pad is None: # calculate from img0_shape
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
@@ -779,61 +854,81 @@ def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
gain = ratio_pad[0][0]
pad = ratio_pad[1]
- coords[:, [0, 2]] -= pad[0] # x padding
- coords[:, [1, 3]] -= pad[1] # y padding
- coords[:, :4] /= gain
- clip_coords(coords, img0_shape)
- return coords
+ segments[:, 0] -= pad[0] # x padding
+ segments[:, 1] -= pad[1] # y padding
+ segments /= gain
+ clip_segments(segments, img0_shape)
+ if normalize:
+ segments[:, 0] /= img0_shape[1] # width
+ segments[:, 1] /= img0_shape[0] # height
+ return segments
-def clip_coords(boxes, shape):
- # Clip bounding xyxy bounding boxes to image shape (height, width)
+def clip_boxes(boxes, shape):
+ # Clip boxes (xyxy) to image shape (height, width)
if isinstance(boxes, torch.Tensor): # faster individually
- boxes[:, 0].clamp_(0, shape[1]) # x1
- boxes[:, 1].clamp_(0, shape[0]) # y1
- boxes[:, 2].clamp_(0, shape[1]) # x2
- boxes[:, 3].clamp_(0, shape[0]) # y2
+ boxes[..., 0].clamp_(0, shape[1]) # x1
+ boxes[..., 1].clamp_(0, shape[0]) # y1
+ boxes[..., 2].clamp_(0, shape[1]) # x2
+ boxes[..., 3].clamp_(0, shape[0]) # y2
else: # np.array (faster grouped)
- boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1]) # x1, x2
- boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0]) # y1, y2
+ boxes[..., [0, 2]] = boxes[..., [0, 2]].clip(0, shape[1]) # x1, x2
+ boxes[..., [1, 3]] = boxes[..., [1, 3]].clip(0, shape[0]) # y1, y2
-def non_max_suppression(prediction,
- conf_thres=0.25,
- iou_thres=0.45,
- classes=None,
- agnostic=False,
- multi_label=False,
- labels=(),
- max_det=300):
- """Non-Maximum Suppression (NMS) on inference results to reject overlapping bounding boxes
+def clip_segments(segments, shape):
+ # Clip segments (xy1,xy2,...) to image shape (height, width)
+ if isinstance(segments, torch.Tensor): # faster individually
+ segments[:, 0].clamp_(0, shape[1]) # x
+ segments[:, 1].clamp_(0, shape[0]) # y
+ else: # np.array (faster grouped)
+ segments[:, 0] = segments[:, 0].clip(0, shape[1]) # x
+ segments[:, 1] = segments[:, 1].clip(0, shape[0]) # y
+
+
+def non_max_suppression(
+ prediction,
+ conf_thres=0.25,
+ iou_thres=0.45,
+ classes=None,
+ agnostic=False,
+ multi_label=False,
+ labels=(),
+ max_det=300,
+ nm=0, # number of masks
+):
+ """Non-Maximum Suppression (NMS) on inference results to reject overlapping detections
Returns:
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
"""
+ # Checks
+ assert 0 <= conf_thres <= 1, f'Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0'
+ assert 0 <= iou_thres <= 1, f'Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0'
if isinstance(prediction, (list, tuple)): # YOLOv5 model in validation model, output = (inference_out, loss_out)
prediction = prediction[0] # select only inference output
+ device = prediction.device
+ mps = 'mps' in device.type # Apple MPS
+ if mps: # MPS not fully supported yet, convert tensors to CPU before NMS
+ prediction = prediction.cpu()
bs = prediction.shape[0] # batch size
- nc = prediction.shape[2] - 5 # number of classes
+ nc = prediction.shape[2] - nm - 5 # number of classes
xc = prediction[..., 4] > conf_thres # candidates
- # Checks
- assert 0 <= conf_thres <= 1, f'Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0'
- assert 0 <= iou_thres <= 1, f'Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0'
-
# Settings
# min_wh = 2 # (pixels) minimum box width and height
max_wh = 7680 # (pixels) maximum box width and height
max_nms = 30000 # maximum number of boxes into torchvision.ops.nms()
- time_limit = 0.3 + 0.03 * bs # seconds to quit after
+ time_limit = 0.5 + 0.05 * bs # seconds to quit after
redundant = True # require redundant detections
multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
merge = False # use merge-NMS
t = time.time()
- output = [torch.zeros((0, 6), device=prediction.device)] * bs
+ mi = 5 + nc # mask start index
+ output = [torch.zeros((0, 6 + nm), device=prediction.device)] * bs
for xi, x in enumerate(prediction): # image index, image inference
# Apply constraints
# x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height
@@ -842,7 +937,7 @@ def non_max_suppression(prediction,
# Cat apriori labels if autolabelling
if labels and len(labels[xi]):
lb = labels[xi]
- v = torch.zeros((len(lb), nc + 5), device=x.device)
+ v = torch.zeros((len(lb), nc + nm + 5), device=x.device)
v[:, :4] = lb[:, 1:5] # box
v[:, 4] = 1.0 # conf
v[range(len(lb)), lb[:, 0].long() + 5] = 1.0 # cls
@@ -855,16 +950,17 @@ def non_max_suppression(prediction,
# Compute conf
x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf
- # Box (center x, center y, width, height) to (x1, y1, x2, y2)
- box = xywh2xyxy(x[:, :4])
+ # Box/Mask
+ box = xywh2xyxy(x[:, :4]) # center_x, center_y, width, height) to (x1, y1, x2, y2)
+ mask = x[:, mi:] # zero columns if no masks
# Detections matrix nx6 (xyxy, conf, cls)
if multi_label:
- i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
- x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
+ i, j = (x[:, 5:mi] > conf_thres).nonzero(as_tuple=False).T
+ x = torch.cat((box[i], x[i, 5 + j, None], j[:, None].float(), mask[i]), 1)
else: # best class only
- conf, j = x[:, 5:].max(1, keepdim=True)
- x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
+ conf, j = x[:, 5:mi].max(1, keepdim=True)
+ x = torch.cat((box, conf, j.float(), mask), 1)[conf.view(-1) > conf_thres]
# Filter by class
if classes is not None:
@@ -878,15 +974,13 @@ def non_max_suppression(prediction,
n = x.shape[0] # number of boxes
if not n: # no boxes
continue
- elif n > max_nms: # excess boxes
- x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence
+ x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence and remove excess boxes
# Batched NMS
c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
- if i.shape[0] > max_det: # limit detections
- i = i[:max_det]
+ i = i[:max_det] # limit detections
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
@@ -896,8 +990,10 @@ def non_max_suppression(prediction,
i = i[iou.sum(1) > 1] # require redundancy
output[xi] = x[i]
+ if mps:
+ output[xi] = output[xi].to(device)
if (time.time() - t) > time_limit:
- LOGGER.warning(f'WARNING: NMS time limit {time_limit:.3f}s exceeded')
+ LOGGER.warning(f'WARNING ⚠️ NMS time limit {time_limit:.3f}s exceeded')
break # time limit exceeded
return output
@@ -908,7 +1004,7 @@ def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_op
x = torch.load(f, map_location=torch.device('cpu'))
if x.get('ema'):
x['model'] = x['ema'] # replace model with ema
- for k in 'optimizer', 'best_fitness', 'wandb_id', 'ema', 'updates': # keys
+ for k in 'optimizer', 'best_fitness', 'ema', 'updates': # keys
x[k] = None
x['epoch'] = -1
x['model'].half() # to FP16
@@ -919,11 +1015,10 @@ def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_op
LOGGER.info(f"Optimizer stripped from {f},{f' saved as {s},' if s else ''} {mb:.1f}MB")
-def print_mutation(results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')):
+def print_mutation(keys, results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')):
evolve_csv = save_dir / 'evolve.csv'
evolve_yaml = save_dir / 'hyp_evolve.yaml'
- keys = ('metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', 'val/box_loss',
- 'val/obj_loss', 'val/cls_loss') + tuple(hyp.keys()) # [results + hyps]
+ keys = tuple(keys) + tuple(hyp.keys()) # [results + hyps]
keys = tuple(x.strip() for x in keys)
vals = results + tuple(hyp.values())
n = len(keys)
@@ -932,7 +1027,7 @@ def print_mutation(results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')):
if bucket:
url = f'gs://{bucket}/evolve.csv'
if gsutil_getsize(url) > (evolve_csv.stat().st_size if evolve_csv.exists() else 0):
- os.system(f'gsutil cp {url} {save_dir}') # download evolve.csv if larger than local
+ subprocess.run(['gsutil', 'cp', f'{url}', f'{save_dir}']) # download evolve.csv if larger than local
# Log to evolve.csv
s = '' if evolve_csv.exists() else (('%20s,' * n % keys).rstrip(',') + '\n') # add header
@@ -941,7 +1036,7 @@ def print_mutation(results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')):
# Save yaml
with open(evolve_yaml, 'w') as f:
- data = pd.read_csv(evolve_csv)
+ data = pd.read_csv(evolve_csv, skipinitialspace=True)
data = data.rename(columns=lambda x: x.strip()) # strip keys
i = np.argmax(fitness(data.values[:, :4])) #
generations = len(data)
@@ -956,7 +1051,7 @@ def print_mutation(results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')):
for x in vals) + '\n\n')
if bucket:
- os.system(f'gsutil cp {evolve_csv} {evolve_yaml} gs://{bucket}') # upload
+ subprocess.run(['gsutil', 'cp', f'{evolve_csv}', f'{evolve_yaml}', f'gs://{bucket}']) # upload
def apply_classifier(x, model, img, im0):
@@ -974,7 +1069,7 @@ def apply_classifier(x, model, img, im0):
d[:, :4] = xywh2xyxy(b).long()
# Rescale boxes from img_size to im0 size
- scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
+ scale_boxes(img.shape[2:], d[:, :4], im0[i].shape)
# Classes
pred_cls1 = d[:, 5].long()
@@ -1020,7 +1115,7 @@ def increment_path(path, exist_ok=False, sep='', mkdir=False):
return path
-# OpenCV Chinese-friendly functions ------------------------------------------------------------------------------------
+# OpenCV Multilanguage-friendly functions ------------------------------------------------------------------------------------
imshow_ = cv2.imshow # copy to avoid recursion errors
@@ -1043,4 +1138,3 @@ def imshow(path, im):
cv2.imread, cv2.imwrite, cv2.imshow = imread, imwrite, imshow # redefine
# Variables ------------------------------------------------------------------------------------------------------------
-NCOLS = 0 if is_docker() else shutil.get_terminal_size().columns # terminal window size for tqdm
diff --git a/utils/google_app_engine/additional_requirements.txt b/utils/google_app_engine/additional_requirements.txt
index 42d7ffc0eed8..d5b76758c876 100644
--- a/utils/google_app_engine/additional_requirements.txt
+++ b/utils/google_app_engine/additional_requirements.txt
@@ -1,4 +1,5 @@
# add these requirements in your app on top of the existing ones
pip==21.1
Flask==1.0.2
-gunicorn==19.9.0
+gunicorn==19.10.0
+werkzeug>=2.2.3 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py
index f29debb76907..9de1f226233c 100644
--- a/utils/loggers/__init__.py
+++ b/utils/loggers/__init__.py
@@ -11,7 +11,7 @@
import torch
from torch.utils.tensorboard import SummaryWriter
-from utils.general import colorstr, cv2
+from utils.general import LOGGER, colorstr, cv2
from utils.loggers.clearml.clearml_utils import ClearmlLogger
from utils.loggers.wandb.wandb_utils import WandbLogger
from utils.plots import plot_images, plot_labels, plot_results
@@ -84,10 +84,6 @@ def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None,
self.csv = True # always log to csv
# Messages
- if not wandb:
- prefix = colorstr('Weights & Biases: ')
- s = f"{prefix}run 'pip install wandb' to automatically track and visualize YOLOv5 🚀 runs in Weights & Biases"
- self.logger.info(s)
if not clearml:
prefix = colorstr('ClearML: ')
s = f"{prefix}run 'pip install clearml' to automatically track, visualize and remotely train YOLOv5 🚀 in ClearML"
@@ -105,27 +101,28 @@ def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None,
# W&B
if wandb and 'wandb' in self.include:
- wandb_artifact_resume = isinstance(self.opt.resume, str) and self.opt.resume.startswith('wandb-artifact://')
- run_id = torch.load(self.weights).get('wandb_id') if self.opt.resume and not wandb_artifact_resume else None
self.opt.hyp = self.hyp # add hyperparameters
- self.wandb = WandbLogger(self.opt, run_id)
- # temp warn. because nested artifacts not supported after 0.12.10
- if pkg.parse_version(wandb.__version__) >= pkg.parse_version('0.12.11'):
- s = "YOLOv5 temporarily requires wandb version 0.12.10 or below. Some features may not work as expected."
- self.logger.warning(s)
+ self.wandb = WandbLogger(self.opt)
else:
self.wandb = None
# ClearML
if clearml and 'clearml' in self.include:
- self.clearml = ClearmlLogger(self.opt, self.hyp)
+ try:
+ self.clearml = ClearmlLogger(self.opt, self.hyp)
+ except Exception:
+ self.clearml = None
+ prefix = colorstr('ClearML: ')
+ LOGGER.warning(f'{prefix}WARNING ⚠️ ClearML is installed but not configured, skipping ClearML logging.'
+ f' See https://github.com/ultralytics/yolov5/tree/master/utils/loggers/clearml#readme')
+
else:
self.clearml = None
# Comet
if comet_ml and 'comet' in self.include:
- if isinstance(self.opt.resume, str) and self.opt.resume.startswith("comet://"):
- run_id = self.opt.resume.split("/")[-1]
+ if isinstance(self.opt.resume, str) and self.opt.resume.startswith('comet://'):
+ run_id = self.opt.resume.split('/')[-1]
self.comet_logger = CometLogger(self.opt, self.hyp, run_id=run_id)
else:
@@ -161,14 +158,14 @@ def on_pretrain_routine_end(self, labels, names):
plot_labels(labels, names, self.save_dir)
paths = self.save_dir.glob('*labels*.jpg') # training labels
if self.wandb:
- self.wandb.log({"Labels": [wandb.Image(str(x), caption=x.name) for x in paths]})
+ self.wandb.log({'Labels': [wandb.Image(str(x), caption=x.name) for x in paths]})
# if self.clearml:
# pass # ClearML saves these images automatically using hooks
if self.comet_logger:
self.comet_logger.on_pretrain_routine_end(paths)
def on_train_batch_end(self, model, ni, imgs, targets, paths, vals):
- log_dict = dict(zip(self.keys[0:3], vals))
+ log_dict = dict(zip(self.keys[:3], vals))
# Callback runs on train batch end
# ni: number integrated batches (since train start)
if self.plots:
@@ -214,10 +211,10 @@ def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
# Callback runs on val end
if self.wandb or self.clearml:
files = sorted(self.save_dir.glob('val*.jpg'))
- if self.wandb:
- self.wandb.log({"Validation": [wandb.Image(str(f), caption=f.name) for f in files]})
- if self.clearml:
- self.clearml.log_debug_samples(files, title='Validation')
+ if self.wandb:
+ self.wandb.log({'Validation': [wandb.Image(str(f), caption=f.name) for f in files]})
+ if self.clearml:
+ self.clearml.log_debug_samples(files, title='Validation')
if self.comet_logger:
self.comet_logger.on_val_end(nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
@@ -246,7 +243,7 @@ def on_fit_epoch_end(self, vals, epoch, best_fitness, fi):
for i, name in enumerate(self.best_keys):
self.wandb.wandb_run.summary[name] = best_results[i] # log best results in the summary
self.wandb.log(x)
- self.wandb.end_epoch(best_result=best_fitness == fi)
+ self.wandb.end_epoch()
if self.clearml:
self.clearml.current_epoch_logged_images = set() # reset epoch image limit
@@ -282,7 +279,7 @@ def on_train_end(self, last, best, epoch, results):
if self.wandb:
self.wandb.log(dict(zip(self.keys[3:10], results)))
- self.wandb.log({"Results": [wandb.Image(str(f), caption=f.name) for f in files]})
+ self.wandb.log({'Results': [wandb.Image(str(f), caption=f.name) for f in files]})
# Calling wandb.log. TODO: Refactor this into WandbLogger.log_model
if not self.opt.evolve:
wandb.log_artifact(str(best if best.exists() else last),
@@ -332,7 +329,7 @@ def __init__(self, opt, console_logger, include=('tb', 'wandb')):
if wandb and 'wandb' in self.include:
self.wandb = wandb.init(project=web_project_name(str(opt.project)),
- name=None if opt.name == "exp" else opt.name,
+ name=None if opt.name == 'exp' else opt.name,
config=opt)
else:
self.wandb = None
@@ -373,12 +370,12 @@ def log_graph(self, model, imgsz=(640, 640)):
def log_model(self, model_path, epoch=0, metadata={}):
# Log model to all loggers
if self.wandb:
- art = wandb.Artifact(name=f"run_{wandb.run.id}_model", type="model", metadata=metadata)
+ art = wandb.Artifact(name=f'run_{wandb.run.id}_model', type='model', metadata=metadata)
art.add_file(str(model_path))
wandb.log_artifact(art)
def update_params(self, params):
- # Update the paramters logged
+ # Update the parameters logged
if self.wandb:
wandb.run.config.update(params, allow_val_change=True)
@@ -393,7 +390,7 @@ def log_tensorboard_graph(tb, model, imgsz=(640, 640)):
warnings.simplefilter('ignore') # suppress jit trace warning
tb.add_graph(torch.jit.trace(de_parallel(model), im, strict=False), [])
except Exception as e:
- print(f'WARNING: TensorBoard graph visualization failure {e}')
+ LOGGER.warning(f'WARNING ⚠️ TensorBoard graph visualization failure {e}')
def web_project_name(project):
diff --git a/utils/loggers/clearml/README.md b/utils/loggers/clearml/README.md
index 64eef6befc93..ca41c040193c 100644
--- a/utils/loggers/clearml/README.md
+++ b/utils/loggers/clearml/README.md
@@ -23,7 +23,6 @@ And so much more. It's up to you how many of these tools you want to use, you ca
![ClearML scalars dashboard](https://github.com/thepycoder/clearml_screenshots/raw/main/experiment_manager_with_compare.gif)
-
@@ -35,15 +34,15 @@ Either sign up for free to the [ClearML Hosted Service](https://cutt.ly/yolov5-t
1. Install the `clearml` python package:
- ```bash
- pip install clearml
- ```
+ ```bash
+ pip install clearml
+ ```
1. Connect the ClearML SDK to the server by [creating credentials](https://app.clear.ml/settings/workspace-configuration) (go right top to Settings -> Workspace -> Create new credentials), then execute the command below and follow the instructions:
- ```bash
- clearml-init
- ```
+ ```bash
+ clearml-init
+ ```
That's it! You're done 😎
@@ -54,16 +53,26 @@ That's it! You're done 😎
To enable ClearML experiment tracking, simply install the ClearML pip package.
```bash
-pip install clearml
+pip install clearml>=1.2.0
```
-This will enable integration with the YOLOv5 training script. Every training run from now on, will be captured and stored by the ClearML experiment manager. If you want to change the `project_name` or `task_name`, head over to our custom logger, where you can change it: `utils/loggers/clearml/clearml_utils.py`
+This will enable integration with the YOLOv5 training script. Every training run from now on, will be captured and stored by the ClearML experiment manager.
+
+If you want to change the `project_name` or `task_name`, use the `--project` and `--name` arguments of the `train.py` script, by default the project will be called `YOLOv5` and the task `Training`.
+PLEASE NOTE: ClearML uses `/` as a delimiter for subprojects, so be careful when using `/` in your project name!
```bash
python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache
```
+or with custom project and task name:
+
+```bash
+python train.py --project my_project --name my_training --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov5s.pt --cache
+```
+
This will capture:
+
- Source code + uncommitted changes
- Installed packages
- (Hyper)parameters
@@ -86,7 +95,7 @@ There even more we can do with all of this information, like hyperparameter opti
## 🔗 Dataset Version Management
-Versioning your data separately from your code is generally a good idea and makes it easy to aqcuire the latest version too. This repository supports supplying a dataset version ID and it will make sure to get the data if it's not there yet. Next to that, this workflow also saves the used dataset ID as part of the task parameters, so you will always know for sure which data was used in which experiment!
+Versioning your data separately from your code is generally a good idea and makes it easy to acquire the latest version too. This repository supports supplying a dataset version ID, and it will make sure to get the data if it's not there yet. Next to that, this workflow also saves the used dataset ID as part of the task parameters, so you will always know for sure which data was used in which experiment!
![ClearML Dataset Interface](https://github.com/thepycoder/clearml_screenshots/raw/main/clearml_data.gif)
@@ -104,6 +113,7 @@ The YOLOv5 repository supports a number of different datasets by using yaml file
|_ LICENSE
|_ README.txt
```
+
But this can be any dataset you wish. Feel free to use your own, as long as you keep to this folder structure.
Next, ⚠️**copy the corresponding yaml file to the root of the dataset folder**⚠️. This yaml files contains the information ClearML will need to properly use the dataset. You can make this yourself too, of course, just follow the structure of the example yamls.
@@ -124,13 +134,15 @@ Basically we need the following keys: `path`, `train`, `test`, `val`, `nc`, `nam
### Upload Your Dataset
-To get this dataset into ClearML as a versionned dataset, go to the dataset root folder and run the following command:
+To get this dataset into ClearML as a versioned dataset, go to the dataset root folder and run the following command:
+
```bash
cd coco128
clearml-data sync --project YOLOv5 --name coco128 --folder .
```
The command `clearml-data sync` is actually a shorthand command. You could also run these commands one after the other:
+
```bash
# Optionally add --parent if you want to base
# this version on another dataset version, so no duplicate files are uploaded!
@@ -169,7 +181,7 @@ python utils/loggers/clearml/hpo.py
## 🤯 Remote Execution (advanced)
-Running HPO locally is really handy, but what if we want to run our experiments on a remote machine instead? Maybe you have access to a very powerful GPU machine on-site or you have some budget to use cloud GPUs.
+Running HPO locally is really handy, but what if we want to run our experiments on a remote machine instead? Maybe you have access to a very powerful GPU machine on-site, or you have some budget to use cloud GPUs.
This is where the ClearML Agent comes into play. Check out what the agent can do here:
- [YouTube video](https://youtu.be/MX3BrXnaULs)
@@ -178,6 +190,7 @@ This is where the ClearML Agent comes into play. Check out what the agent can do
In short: every experiment tracked by the experiment manager contains enough information to reproduce it on a different machine (installed packages, uncommitted changes etc.). So a ClearML agent does just that: it listens to a queue for incoming tasks and when it finds one, it recreates the environment and runs it while still reporting scalars, plots etc. to the experiment manager.
You can turn any machine (a cloud VM, a local GPU machine, your own laptop ... ) into a ClearML agent by simply running:
+
```bash
clearml-agent daemon --queue [--docker]
```
@@ -186,11 +199,11 @@ clearml-agent daemon --queue [--docker]
With our agent running, we can give it some work. Remember from the HPO section that we can clone a task and edit the hyperparameters? We can do that from the interface too!
-🪄 Clone the experiment by right clicking it
+🪄 Clone the experiment by right-clicking it
🎯 Edit the hyperparameters to what you wish them to be
-⏳ Enqueue the task to any of the queues by right clicking it
+⏳ Enqueue the task to any of the queues by right-clicking it
![Enqueue a task from the UI](https://github.com/thepycoder/clearml_screenshots/raw/main/enqueue.gif)
@@ -198,7 +211,8 @@ With our agent running, we can give it some work. Remember from the HPO section
Now you can clone a task like we explained above, or simply mark your current script by adding `task.execute_remotely()` and on execution it will be put into a queue, for the agent to start working on!
-To run the YOLOv5 training script remotely, all you have to do is add this line to the training.py script after the clearml logger has been instatiated:
+To run the YOLOv5 training script remotely, all you have to do is add this line to the training.py script after the clearml logger has been instantiated:
+
```python
# ...
# Loggers
@@ -206,16 +220,17 @@ data_dict = None
if RANK in {-1, 0}:
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance
if loggers.clearml:
- loggers.clearml.task.execute_remotely(queue='my_queue') # <------ ADD THIS LINE
+ loggers.clearml.task.execute_remotely(queue="my_queue") # <------ ADD THIS LINE
# Data_dict is either None is user did not choose for ClearML dataset or is filled in by ClearML
data_dict = loggers.clearml.data_dict
# ...
```
+
When running the training script after this change, python will run the script up until that line, after which it will package the code and send it to the queue instead!
### Autoscaling workers
-ClearML comes with autoscalers too! This tool will automatically spin up new remote machines in the cloud of your choice (AWS, GCP, Azure) and turn them into ClearML agents for you whenever there are experiments detected in the queue. Once the tasks are processed, the autoscaler will automatically shut down the remote machines and you stop paying!
+ClearML comes with autoscalers too! This tool will automatically spin up new remote machines in the cloud of your choice (AWS, GCP, Azure) and turn them into ClearML agents for you whenever there are experiments detected in the queue. Once the tasks are processed, the autoscaler will automatically shut down the remote machines, and you stop paying!
Check out the autoscalers getting started video below.
diff --git a/utils/loggers/clearml/clearml_utils.py b/utils/loggers/clearml/clearml_utils.py
index 1e136907367d..2764abe90da8 100644
--- a/utils/loggers/clearml/clearml_utils.py
+++ b/utils/loggers/clearml/clearml_utils.py
@@ -11,6 +11,7 @@
try:
import clearml
from clearml import Dataset, Task
+
assert hasattr(clearml, '__version__') # verify package import not local dir
except (ImportError, AssertionError):
clearml = None
@@ -24,7 +25,7 @@ def construct_dataset(clearml_info_string):
dataset_root_path = Path(dataset.get_local_copy())
# We'll search for the yaml file definition in the dataset
- yaml_filenames = list(glob.glob(str(dataset_root_path / "*.yaml")) + glob.glob(str(dataset_root_path / "*.yml")))
+ yaml_filenames = list(glob.glob(str(dataset_root_path / '*.yaml')) + glob.glob(str(dataset_root_path / '*.yml')))
if len(yaml_filenames) > 1:
raise ValueError('More than one yaml file was found in the dataset root, cannot determine which one contains '
'the dataset definition this way.')
@@ -84,10 +85,11 @@ def __init__(self, opt, hyp):
self.data_dict = None
if self.clearml:
self.task = Task.init(
- project_name='YOLOv5',
- task_name='training',
+ project_name=opt.project if opt.project != 'runs/train' else 'YOLOv5',
+ task_name=opt.name if opt.name != 'exp' else 'Training',
tags=['YOLOv5'],
output_uri=True,
+ reuse_last_task_id=opt.exist_ok,
auto_connect_frameworks={'pytorch': False}
# We disconnect pytorch auto-detection, because we added manual model save points in the code
)
@@ -95,6 +97,12 @@ def __init__(self, opt, hyp):
# Only the hyperparameters coming from the yaml config file
# will have to be added manually!
self.task.connect(hyp, name='Hyperparameters')
+ self.task.connect(opt, name='Args')
+
+ # Make sure the code is easily remotely runnable by setting the docker image to use by the remote agent
+ self.task.set_base_docker('ultralytics/yolov5:latest',
+ docker_arguments='--ipc=host -e="CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1"',
+ docker_setup_bash_script='pip install clearml')
# Get ClearML Dataset Version if requested
if opt.data.startswith('clearml://'):
@@ -142,7 +150,7 @@ def log_image_with_boxes(self, image_path, boxes, class_names, image, conf_thres
class_name = class_names[int(class_nr)]
confidence_percentage = round(float(conf) * 100, 2)
- label = f"{class_name}: {confidence_percentage}%"
+ label = f'{class_name}: {confidence_percentage}%'
if conf > conf_threshold:
annotator.rectangle(box.cpu().numpy(), outline=color)
diff --git a/utils/loggers/comet/README.md b/utils/loggers/comet/README.md
index 7b0b8e0e2f09..47e6a45654b8 100644
--- a/utils/loggers/comet/README.md
+++ b/utils/loggers/comet/README.md
@@ -2,13 +2,13 @@
# YOLOv5 with Comet
-This guide will cover how to use YOLOv5 with [Comet](https://www.comet.com/site/?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)
+This guide will cover how to use YOLOv5 with [Comet](https://bit.ly/yolov5-readme-comet2)
# About Comet
Comet builds tools that help data scientists, engineers, and team leaders accelerate and optimize machine learning and deep learning models.
-Track and visualize model metrics in real time, save your hyperparameters, datasets, and model checkpoints, and visualize your model predictions with [Comet Custom Panels](https://www.comet.com/examples/comet-example-yolov5?shareable=YcwMiJaZSXfcEXpGOHDD12vA1&ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)!
+Track and visualize model metrics in real time, save your hyperparameters, datasets, and model checkpoints, and visualize your model predictions with [Comet Custom Panels](https://www.comet.com/docs/v2/guides/comet-dashboard/code-panels/about-panels/?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)!
Comet makes sure you never lose track of your work and makes it easy to share results and collaborate across teams of all sizes!
# Getting Started
@@ -23,7 +23,7 @@ pip install comet_ml
There are two ways to configure Comet with YOLOv5.
-You can either set your credentials through enviroment variables
+You can either set your credentials through environment variables
**Environment Variables**
@@ -49,12 +49,13 @@ project_name= # This will default to 'yolov5'
python train.py --img 640 --batch 16 --epochs 5 --data coco128.yaml --weights yolov5s.pt
```
-That's it! Comet will automatically log your hyperparameters, command line arguments, training and valiation metrics. You can visualize and analyze your runs in the Comet UI
+That's it! Comet will automatically log your hyperparameters, command line arguments, training and validation metrics. You can visualize and analyze your runs in the Comet UI
-
+
# Try out an Example!
-Check out an example of a [completed run here](https://www.comet.com/examples/comet-example-yolov5/a0e29e0e9b984e4a822db2a62d0cb357?experiment-tab=chart&showOutliers=true&smoothing=0&transformY=smoothing&xAxis=step&ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)
+
+Check out an example of a [completed run here](https://www.comet.com/examples/comet-example-yolov5/a0e29e0e9b984e4a822db2a62d0cb357?experiment-tab=chart&showOutliers=true&smoothing=0&transformY=smoothing&xAxis=step&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)
Or better yet, try it out yourself in this Colab Notebook
@@ -65,6 +66,7 @@ Or better yet, try it out yourself in this Colab Notebook
By default, Comet will log the following items
## Metrics
+
- Box Loss, Object Loss, Classification Loss for the training and validation data
- mAP_0.5, mAP_0.5:0.95 metrics for the validation data.
- Precision and Recall for the validation data
@@ -119,8 +121,7 @@ You can control the frequency of logged predictions and the associated images by
**Note:** The YOLOv5 validation dataloader will default to a batch size of 32, so you will have to set the logging frequency accordingly.
-Here is an [example project using the Panel](https://www.comet.com/examples/comet-example-yolov5?shareable=YcwMiJaZSXfcEXpGOHDD12vA1&ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)
-
+Here is an [example project using the Panel](https://www.comet.com/examples/comet-example-yolov5?shareable=YcwMiJaZSXfcEXpGOHDD12vA1&utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)
```shell
python train.py \
@@ -161,7 +162,7 @@ env COMET_LOG_PER_CLASS_METRICS=true python train.py \
## Uploading a Dataset to Comet Artifacts
-If you would like to store your data using [Comet Artifacts](https://www.comet.com/docs/v2/guides/data-management/using-artifacts/#learn-more?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration), you can do so using the `upload_dataset` flag.
+If you would like to store your data using [Comet Artifacts](https://www.comet.com/docs/v2/guides/data-management/using-artifacts/#learn-more?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github), you can do so using the `upload_dataset` flag.
The dataset be organized in the way described in the [YOLOv5 documentation](https://docs.ultralytics.com/tutorials/train-custom-datasets/#3-organize-directories). The dataset config `yaml` file must follow the same format as that of the `coco128.yaml` file.
@@ -192,6 +193,7 @@ If you would like to use a dataset from Comet Artifacts, set the `path` variable
# contents of artifact.yaml file
path: "comet:///:"
```
+
Then pass this file to your training script in the following way
```shell
@@ -221,7 +223,7 @@ python train.py \
## Hyperparameter Search with the Comet Optimizer
-YOLOv5 is also integrated with Comet's Optimizer, making is simple to visualie hyperparameter sweeps in the Comet UI.
+YOLOv5 is also integrated with Comet's Optimizer, making is simple to visualize hyperparameter sweeps in the Comet UI.
### Configuring an Optimizer Sweep
@@ -251,6 +253,6 @@ comet optimizer -j utils/loggers/comet/hpo.py \
### Visualizing Results
-Comet provides a number of ways to visualize the results of your sweep. Take a look at a [project with a completed sweep here](https://www.comet.com/examples/comet-example-yolov5/view/PrlArHGuuhDTKC1UuBmTtOSXD/panels?ref=yolov5&utm_source=yolov5&utm_medium=affilliate&utm_campaign=yolov5_comet_integration)
+Comet provides a number of ways to visualize the results of your sweep. Take a look at a [project with a completed sweep here](https://www.comet.com/examples/comet-example-yolov5/view/PrlArHGuuhDTKC1UuBmTtOSXD/panels?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github)
-
\ No newline at end of file
+
diff --git a/utils/loggers/comet/__init__.py b/utils/loggers/comet/__init__.py
index 4ee86dd70d6e..d4599841c9fc 100644
--- a/utils/loggers/comet/__init__.py
+++ b/utils/loggers/comet/__init__.py
@@ -17,45 +17,46 @@
# Project Configuration
config = comet_ml.config.get_config()
- COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5")
+ COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5')
except (ModuleNotFoundError, ImportError):
comet_ml = None
COMET_PROJECT_NAME = None
+import PIL
import torch
import torchvision.transforms as T
import yaml
from utils.dataloaders import img2label_paths
-from utils.general import check_dataset, scale_coords, xywh2xyxy
+from utils.general import check_dataset, scale_boxes, xywh2xyxy
from utils.metrics import box_iou
-COMET_PREFIX = "comet://"
+COMET_PREFIX = 'comet://'
-COMET_MODE = os.getenv("COMET_MODE", "online")
+COMET_MODE = os.getenv('COMET_MODE', 'online')
# Model Saving Settings
-COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5")
+COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5')
# Dataset Artifact Settings
-COMET_UPLOAD_DATASET = os.getenv("COMET_UPLOAD_DATASET", "false").lower() == "true"
+COMET_UPLOAD_DATASET = os.getenv('COMET_UPLOAD_DATASET', 'false').lower() == 'true'
# Evaluation Settings
-COMET_LOG_CONFUSION_MATRIX = os.getenv("COMET_LOG_CONFUSION_MATRIX", "true").lower() == "true"
-COMET_LOG_PREDICTIONS = os.getenv("COMET_LOG_PREDICTIONS", "true").lower() == "true"
-COMET_MAX_IMAGE_UPLOADS = int(os.getenv("COMET_MAX_IMAGE_UPLOADS", 100))
+COMET_LOG_CONFUSION_MATRIX = os.getenv('COMET_LOG_CONFUSION_MATRIX', 'true').lower() == 'true'
+COMET_LOG_PREDICTIONS = os.getenv('COMET_LOG_PREDICTIONS', 'true').lower() == 'true'
+COMET_MAX_IMAGE_UPLOADS = int(os.getenv('COMET_MAX_IMAGE_UPLOADS', 100))
# Confusion Matrix Settings
-CONF_THRES = float(os.getenv("CONF_THRES", 0.001))
-IOU_THRES = float(os.getenv("IOU_THRES", 0.6))
+CONF_THRES = float(os.getenv('CONF_THRES', 0.001))
+IOU_THRES = float(os.getenv('IOU_THRES', 0.6))
# Batch Logging Settings
-COMET_LOG_BATCH_METRICS = os.getenv("COMET_LOG_BATCH_METRICS", "false").lower() == "true"
-COMET_BATCH_LOGGING_INTERVAL = os.getenv("COMET_BATCH_LOGGING_INTERVAL", 1)
-COMET_PREDICTION_LOGGING_INTERVAL = os.getenv("COMET_PREDICTION_LOGGING_INTERVAL", 1)
-COMET_LOG_PER_CLASS_METRICS = os.getenv("COMET_LOG_PER_CLASS_METRICS", "false").lower() == "true"
+COMET_LOG_BATCH_METRICS = os.getenv('COMET_LOG_BATCH_METRICS', 'false').lower() == 'true'
+COMET_BATCH_LOGGING_INTERVAL = os.getenv('COMET_BATCH_LOGGING_INTERVAL', 1)
+COMET_PREDICTION_LOGGING_INTERVAL = os.getenv('COMET_PREDICTION_LOGGING_INTERVAL', 1)
+COMET_LOG_PER_CLASS_METRICS = os.getenv('COMET_LOG_PER_CLASS_METRICS', 'false').lower() == 'true'
-RANK = int(os.getenv("RANK", -1))
+RANK = int(os.getenv('RANK', -1))
to_pil = T.ToPILImage()
@@ -65,7 +66,7 @@ class CometLogger:
with Comet
"""
- def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwargs) -> None:
+ def __init__(self, opt, hyp, run_id=None, job_type='Training', **experiment_kwargs) -> None:
self.job_type = job_type
self.opt = opt
self.hyp = hyp
@@ -86,51 +87,53 @@ def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwar
# Default parameters to pass to Experiment objects
self.default_experiment_kwargs = {
- "log_code": False,
- "log_env_gpu": True,
- "log_env_cpu": True,
- "project_name": COMET_PROJECT_NAME,}
+ 'log_code': False,
+ 'log_env_gpu': True,
+ 'log_env_cpu': True,
+ 'project_name': COMET_PROJECT_NAME,}
self.default_experiment_kwargs.update(experiment_kwargs)
self.experiment = self._get_experiment(self.comet_mode, run_id)
self.data_dict = self.check_dataset(self.opt.data)
- self.class_names = self.data_dict["names"]
- self.num_classes = self.data_dict["nc"]
+ self.class_names = self.data_dict['names']
+ self.num_classes = self.data_dict['nc']
self.logged_images_count = 0
self.max_images = COMET_MAX_IMAGE_UPLOADS
if run_id is None:
- self.experiment.log_other("Created from", "YOLOv5")
+ self.experiment.log_other('Created from', 'YOLOv5')
if not isinstance(self.experiment, comet_ml.OfflineExperiment):
- workspace, project_name, experiment_id = self.experiment.url.split("/")[-3:]
+ workspace, project_name, experiment_id = self.experiment.url.split('/')[-3:]
self.experiment.log_other(
- "Run Path",
- f"{workspace}/{project_name}/{experiment_id}",
+ 'Run Path',
+ f'{workspace}/{project_name}/{experiment_id}',
)
self.log_parameters(vars(opt))
self.log_parameters(self.opt.hyp)
self.log_asset_data(
self.opt.hyp,
- name="hyperparameters.json",
- metadata={"type": "hyp-config-file"},
+ name='hyperparameters.json',
+ metadata={'type': 'hyp-config-file'},
)
self.log_asset(
- f"{self.opt.save_dir}/opt.yaml",
- metadata={"type": "opt-config-file"},
+ f'{self.opt.save_dir}/opt.yaml',
+ metadata={'type': 'opt-config-file'},
)
self.comet_log_confusion_matrix = COMET_LOG_CONFUSION_MATRIX
- if hasattr(self.opt, "conf_thres"):
+ if hasattr(self.opt, 'conf_thres'):
self.conf_thres = self.opt.conf_thres
else:
self.conf_thres = CONF_THRES
- if hasattr(self.opt, "iou_thres"):
+ if hasattr(self.opt, 'iou_thres'):
self.iou_thres = self.opt.iou_thres
else:
self.iou_thres = IOU_THRES
+ self.log_parameters({'val_iou_threshold': self.iou_thres, 'val_conf_threshold': self.conf_thres})
+
self.comet_log_predictions = COMET_LOG_PREDICTIONS
if self.opt.bbox_interval == -1:
self.comet_log_prediction_interval = 1 if self.opt.epochs < 10 else self.opt.epochs // 10
@@ -139,26 +142,27 @@ def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwar
if self.comet_log_predictions:
self.metadata_dict = {}
+ self.logged_image_names = []
self.comet_log_per_class_metrics = COMET_LOG_PER_CLASS_METRICS
self.experiment.log_others({
- "comet_mode": COMET_MODE,
- "comet_max_image_uploads": COMET_MAX_IMAGE_UPLOADS,
- "comet_log_per_class_metrics": COMET_LOG_PER_CLASS_METRICS,
- "comet_log_batch_metrics": COMET_LOG_BATCH_METRICS,
- "comet_log_confusion_matrix": COMET_LOG_CONFUSION_MATRIX,
- "comet_model_name": COMET_MODEL_NAME,})
+ 'comet_mode': COMET_MODE,
+ 'comet_max_image_uploads': COMET_MAX_IMAGE_UPLOADS,
+ 'comet_log_per_class_metrics': COMET_LOG_PER_CLASS_METRICS,
+ 'comet_log_batch_metrics': COMET_LOG_BATCH_METRICS,
+ 'comet_log_confusion_matrix': COMET_LOG_CONFUSION_MATRIX,
+ 'comet_model_name': COMET_MODEL_NAME,})
# Check if running the Experiment with the Comet Optimizer
- if hasattr(self.opt, "comet_optimizer_id"):
- self.experiment.log_other("optimizer_id", self.opt.comet_optimizer_id)
- self.experiment.log_other("optimizer_objective", self.opt.comet_optimizer_objective)
- self.experiment.log_other("optimizer_metric", self.opt.comet_optimizer_metric)
- self.experiment.log_other("optimizer_parameters", json.dumps(self.hyp))
+ if hasattr(self.opt, 'comet_optimizer_id'):
+ self.experiment.log_other('optimizer_id', self.opt.comet_optimizer_id)
+ self.experiment.log_other('optimizer_objective', self.opt.comet_optimizer_objective)
+ self.experiment.log_other('optimizer_metric', self.opt.comet_optimizer_metric)
+ self.experiment.log_other('optimizer_parameters', json.dumps(self.hyp))
def _get_experiment(self, mode, experiment_id=None):
- if mode == "offline":
+ if mode == 'offline':
if experiment_id is not None:
return comet_ml.ExistingOfflineExperiment(
previous_experiment=experiment_id,
@@ -178,11 +182,11 @@ def _get_experiment(self, mode, experiment_id=None):
return comet_ml.Experiment(**self.default_experiment_kwargs)
except ValueError:
- logger.warning("COMET WARNING: "
- "Comet credentials have not been set. "
- "Comet will default to offline logging. "
- "Please set your credentials to enable online logging.")
- return self._get_experiment("offline", experiment_id)
+ logger.warning('COMET WARNING: '
+ 'Comet credentials have not been set. '
+ 'Comet will default to offline logging. '
+ 'Please set your credentials to enable online logging.')
+ return self._get_experiment('offline', experiment_id)
return
@@ -206,12 +210,12 @@ def log_model(self, path, opt, epoch, fitness_score, best_model=False):
return
model_metadata = {
- "fitness_score": fitness_score[-1],
- "epochs_trained": epoch + 1,
- "save_period": opt.save_period,
- "total_epochs": opt.epochs,}
+ 'fitness_score': fitness_score[-1],
+ 'epochs_trained': epoch + 1,
+ 'save_period': opt.save_period,
+ 'total_epochs': opt.epochs,}
- model_files = glob.glob(f"{path}/*.pt")
+ model_files = glob.glob(f'{path}/*.pt')
for model_path in model_files:
name = Path(model_path).name
@@ -228,12 +232,12 @@ def check_dataset(self, data_file):
data_config = yaml.safe_load(f)
if data_config['path'].startswith(COMET_PREFIX):
- path = data_config['path'].replace(COMET_PREFIX, "")
+ path = data_config['path'].replace(COMET_PREFIX, '')
data_dict = self.download_dataset_artifact(path)
return data_dict
- self.log_asset(self.opt.data, metadata={"type": "data-config-file"})
+ self.log_asset(self.opt.data, metadata={'type': 'data-config-file'})
return check_dataset(data_file)
@@ -249,31 +253,32 @@ def log_predictions(self, image, labelsn, path, shape, predn):
filtered_detections = detections[mask]
filtered_labels = labelsn[mask]
- processed_image = (image * 255).to(torch.uint8)
-
- image_id = path.split("/")[-1].split(".")[0]
- image_name = f"{image_id}_curr_epoch_{self.experiment.curr_epoch}"
- self.log_image(to_pil(processed_image), name=image_name)
+ image_id = path.split('/')[-1].split('.')[0]
+ image_name = f'{image_id}_curr_epoch_{self.experiment.curr_epoch}'
+ if image_name not in self.logged_image_names:
+ native_scale_image = PIL.Image.open(path)
+ self.log_image(native_scale_image, name=image_name)
+ self.logged_image_names.append(image_name)
metadata = []
for cls, *xyxy in filtered_labels.tolist():
metadata.append({
- "label": f"{self.class_names[int(cls)]}-gt",
- "score": 100,
- "box": {
- "x": xyxy[0],
- "y": xyxy[1],
- "x2": xyxy[2],
- "y2": xyxy[3]},})
+ 'label': f'{self.class_names[int(cls)]}-gt',
+ 'score': 100,
+ 'box': {
+ 'x': xyxy[0],
+ 'y': xyxy[1],
+ 'x2': xyxy[2],
+ 'y2': xyxy[3]},})
for *xyxy, conf, cls in filtered_detections.tolist():
metadata.append({
- "label": f"{self.class_names[int(cls)]}",
- "score": conf * 100,
- "box": {
- "x": xyxy[0],
- "y": xyxy[1],
- "x2": xyxy[2],
- "y2": xyxy[3]},})
+ 'label': f'{self.class_names[int(cls)]}',
+ 'score': conf * 100,
+ 'box': {
+ 'x': xyxy[0],
+ 'y': xyxy[1],
+ 'x2': xyxy[2],
+ 'y2': xyxy[3]},})
self.metadata_dict[image_name] = metadata
self.logged_images_count += 1
@@ -288,47 +293,47 @@ def preprocess_prediction(self, image, labels, shape, pred):
pred[:, 5] = 0
predn = pred.clone()
- scale_coords(image.shape[1:], predn[:, :4], shape[0], shape[1])
+ scale_boxes(image.shape[1:], predn[:, :4], shape[0], shape[1])
labelsn = None
if nl:
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
- scale_coords(image.shape[1:], tbox, shape[0], shape[1]) # native-space labels
+ scale_boxes(image.shape[1:], tbox, shape[0], shape[1]) # native-space labels
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
- scale_coords(image.shape[1:], predn[:, :4], shape[0], shape[1]) # native-space pred
+ scale_boxes(image.shape[1:], predn[:, :4], shape[0], shape[1]) # native-space pred
return predn, labelsn
def add_assets_to_artifact(self, artifact, path, asset_path, split):
- img_paths = sorted(glob.glob(f"{asset_path}/*"))
+ img_paths = sorted(glob.glob(f'{asset_path}/*'))
label_paths = img2label_paths(img_paths)
for image_file, label_file in zip(img_paths, label_paths):
image_logical_path, label_logical_path = map(lambda x: os.path.relpath(x, path), [image_file, label_file])
try:
- artifact.add(image_file, logical_path=image_logical_path, metadata={"split": split})
- artifact.add(label_file, logical_path=label_logical_path, metadata={"split": split})
+ artifact.add(image_file, logical_path=image_logical_path, metadata={'split': split})
+ artifact.add(label_file, logical_path=label_logical_path, metadata={'split': split})
except ValueError as e:
logger.error('COMET ERROR: Error adding file to Artifact. Skipping file.')
- logger.error(f"COMET ERROR: {e}")
+ logger.error(f'COMET ERROR: {e}')
continue
return artifact
def upload_dataset_artifact(self):
- dataset_name = self.data_dict.get("dataset_name", "yolov5-dataset")
- path = str((ROOT / Path(self.data_dict["path"])).resolve())
+ dataset_name = self.data_dict.get('dataset_name', 'yolov5-dataset')
+ path = str((ROOT / Path(self.data_dict['path'])).resolve())
metadata = self.data_dict.copy()
- for key in ["train", "val", "test"]:
+ for key in ['train', 'val', 'test']:
split_path = metadata.get(key)
if split_path is not None:
- metadata[key] = split_path.replace(path, "")
+ metadata[key] = split_path.replace(path, '')
- artifact = comet_ml.Artifact(name=dataset_name, artifact_type="dataset", metadata=metadata)
+ artifact = comet_ml.Artifact(name=dataset_name, artifact_type='dataset', metadata=metadata)
for key in metadata.keys():
- if key in ["train", "val", "test"]:
+ if key in ['train', 'val', 'test']:
if isinstance(self.upload_dataset, str) and (key != self.upload_dataset):
continue
@@ -347,20 +352,27 @@ def download_dataset_artifact(self, artifact_path):
metadata = logged_artifact.metadata
data_dict = metadata.copy()
- data_dict["path"] = artifact_save_dir
- data_dict["names"] = {int(k): v for k, v in metadata.get("names").items()}
+ data_dict['path'] = artifact_save_dir
+
+ metadata_names = metadata.get('names')
+ if type(metadata_names) == dict:
+ data_dict['names'] = {int(k): v for k, v in metadata.get('names').items()}
+ elif type(metadata_names) == list:
+ data_dict['names'] = {int(k): v for k, v in zip(range(len(metadata_names)), metadata_names)}
+ else:
+ raise "Invalid 'names' field in dataset yaml file. Please use a list or dictionary"
data_dict = self.update_data_paths(data_dict)
return data_dict
def update_data_paths(self, data_dict):
- path = data_dict.get("path", "")
+ path = data_dict.get('path', '')
- for split in ["train", "val", "test"]:
+ for split in ['train', 'val', 'test']:
if data_dict.get(split):
split_path = data_dict.get(split)
- data_dict[split] = (f"{path}/{split_path}" if isinstance(split, str) else [
- f"{path}/{x}" for x in split_path])
+ data_dict[split] = (f'{path}/{split_path}' if isinstance(split, str) else [
+ f'{path}/{x}' for x in split_path])
return data_dict
@@ -401,11 +413,11 @@ def on_train_batch_end(self, log_dict, step):
def on_train_end(self, files, save_dir, last, best, epoch, results):
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)
+ self.experiment.log_asset_data(self.metadata_dict, 'image-metadata.json', epoch=curr_epoch)
for f in files:
- self.log_asset(f, metadata={"epoch": epoch})
- self.log_asset(f"{save_dir}/results.csv", metadata={"epoch": epoch})
+ self.log_asset(f, metadata={'epoch': epoch})
+ self.log_asset(f'{save_dir}/results.csv', metadata={'epoch': epoch})
if not self.opt.evolve:
model_path = str(best if best.exists() else last)
@@ -469,7 +481,7 @@ def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
if self.comet_log_confusion_matrix:
epoch = self.experiment.curr_epoch
class_names = list(self.class_names.values())
- class_names.append("background")
+ class_names.append('background')
num_classes = len(class_names)
self.experiment.log_confusion_matrix(
@@ -479,7 +491,7 @@ def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
epoch=epoch,
column_label='Actual Category',
row_label='Predicted Category',
- file_name=f"confusion-matrix-epoch-{epoch}.json",
+ file_name=f'confusion-matrix-epoch-{epoch}.json',
)
def on_fit_epoch_end(self, result, epoch):
diff --git a/utils/loggers/comet/comet_utils.py b/utils/loggers/comet/comet_utils.py
index 3cbd45156b57..27600761ad28 100644
--- a/utils/loggers/comet/comet_utils.py
+++ b/utils/loggers/comet/comet_utils.py
@@ -11,28 +11,28 @@
logger = logging.getLogger(__name__)
-COMET_PREFIX = "comet://"
-COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5")
-COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv("COMET_DEFAULT_CHECKPOINT_FILENAME", "last.pt")
+COMET_PREFIX = 'comet://'
+COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5')
+COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv('COMET_DEFAULT_CHECKPOINT_FILENAME', 'last.pt')
def download_model_checkpoint(opt, experiment):
- model_dir = f"{opt.project}/{experiment.name}"
+ model_dir = f'{opt.project}/{experiment.name}'
os.makedirs(model_dir, exist_ok=True)
model_name = COMET_MODEL_NAME
model_asset_list = experiment.get_model_asset_list(model_name)
if len(model_asset_list) == 0:
- logger.error(f"COMET ERROR: No checkpoints found for model name : {model_name}")
+ logger.error(f'COMET ERROR: No checkpoints found for model name : {model_name}')
return
model_asset_list = sorted(
model_asset_list,
- key=lambda x: x["step"],
+ key=lambda x: x['step'],
reverse=True,
)
- logged_checkpoint_map = {asset["fileName"]: asset["assetId"] for asset in model_asset_list}
+ logged_checkpoint_map = {asset['fileName']: asset['assetId'] for asset in model_asset_list}
resource_url = urlparse(opt.weights)
checkpoint_filename = resource_url.query
@@ -44,22 +44,22 @@ def download_model_checkpoint(opt, experiment):
checkpoint_filename = COMET_DEFAULT_CHECKPOINT_FILENAME
if asset_id is None:
- logger.error(f"COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment")
+ logger.error(f'COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment')
return
try:
- logger.info(f"COMET INFO: Downloading checkpoint {checkpoint_filename}")
+ logger.info(f'COMET INFO: Downloading checkpoint {checkpoint_filename}')
asset_filename = checkpoint_filename
- model_binary = experiment.get_asset(asset_id, return_type="binary", stream=False)
- model_download_path = f"{model_dir}/{asset_filename}"
- with open(model_download_path, "wb") as f:
+ model_binary = experiment.get_asset(asset_id, return_type='binary', stream=False)
+ model_download_path = f'{model_dir}/{asset_filename}'
+ with open(model_download_path, 'wb') as f:
f.write(model_binary)
opt.weights = model_download_path
except Exception as e:
- logger.warning("COMET WARNING: Unable to download checkpoint from Comet")
+ logger.warning('COMET WARNING: Unable to download checkpoint from Comet')
logger.exception(e)
@@ -75,9 +75,9 @@ def set_opt_parameters(opt, experiment):
resume_string = opt.resume
for asset in asset_list:
- if asset["fileName"] == "opt.yaml":
- asset_id = asset["assetId"]
- asset_binary = experiment.get_asset(asset_id, return_type="binary", stream=False)
+ if asset['fileName'] == 'opt.yaml':
+ asset_id = asset['assetId']
+ asset_binary = experiment.get_asset(asset_id, return_type='binary', stream=False)
opt_dict = yaml.safe_load(asset_binary)
for key, value in opt_dict.items():
setattr(opt, key, value)
@@ -85,11 +85,11 @@ def set_opt_parameters(opt, experiment):
# Save hyperparameters to YAML file
# Necessary to pass checks in training script
- save_dir = f"{opt.project}/{experiment.name}"
+ save_dir = f'{opt.project}/{experiment.name}'
os.makedirs(save_dir, exist_ok=True)
- hyp_yaml_path = f"{save_dir}/hyp.yaml"
- with open(hyp_yaml_path, "w") as f:
+ hyp_yaml_path = f'{save_dir}/hyp.yaml'
+ with open(hyp_yaml_path, 'w') as f:
yaml.dump(opt.hyp, f)
opt.hyp = hyp_yaml_path
@@ -113,7 +113,7 @@ def check_comet_weights(opt):
if opt.weights.startswith(COMET_PREFIX):
api = comet_ml.API()
resource = urlparse(opt.weights)
- experiment_path = f"{resource.netloc}{resource.path}"
+ experiment_path = f'{resource.netloc}{resource.path}'
experiment = api.get(experiment_path)
download_model_checkpoint(opt, experiment)
return True
@@ -140,7 +140,7 @@ def check_comet_resume(opt):
if opt.resume.startswith(COMET_PREFIX):
api = comet_ml.API()
resource = urlparse(opt.resume)
- experiment_path = f"{resource.netloc}{resource.path}"
+ experiment_path = f'{resource.netloc}{resource.path}'
experiment = api.get(experiment_path)
set_opt_parameters(opt, experiment)
download_model_checkpoint(opt, experiment)
diff --git a/utils/loggers/comet/hpo.py b/utils/loggers/comet/hpo.py
index eab4df9978cf..fc49115c1358 100644
--- a/utils/loggers/comet/hpo.py
+++ b/utils/loggers/comet/hpo.py
@@ -14,14 +14,14 @@
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
-from train import parse_opt, train
+from train import train
from utils.callbacks import Callbacks
from utils.general import increment_path
from utils.torch_utils import select_device
# Project Configuration
config = comet_ml.config.get_config()
-COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5")
+COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5')
def get_args(known=False):
@@ -68,30 +68,30 @@ def get_args(known=False):
parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')
# Comet Arguments
- parser.add_argument("--comet_optimizer_config", type=str, help="Comet: Path to a Comet Optimizer Config File.")
- parser.add_argument("--comet_optimizer_id", type=str, help="Comet: ID of the Comet Optimizer sweep.")
- parser.add_argument("--comet_optimizer_objective", type=str, help="Comet: Set to 'minimize' or 'maximize'.")
- parser.add_argument("--comet_optimizer_metric", type=str, help="Comet: Metric to Optimize.")
- parser.add_argument("--comet_optimizer_workers",
+ parser.add_argument('--comet_optimizer_config', type=str, help='Comet: Path to a Comet Optimizer Config File.')
+ parser.add_argument('--comet_optimizer_id', type=str, help='Comet: ID of the Comet Optimizer sweep.')
+ parser.add_argument('--comet_optimizer_objective', type=str, help="Comet: Set to 'minimize' or 'maximize'.")
+ parser.add_argument('--comet_optimizer_metric', type=str, help='Comet: Metric to Optimize.')
+ parser.add_argument('--comet_optimizer_workers',
type=int,
default=1,
- help="Comet: Number of Parallel Workers to use with the Comet Optimizer.")
+ help='Comet: Number of Parallel Workers to use with the Comet Optimizer.')
return parser.parse_known_args()[0] if known else parser.parse_args()
def run(parameters, opt):
- hyp_dict = {k: v for k, v in parameters.items() if k not in ["epochs", "batch_size"]}
+ 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))
- opt.batch_size = parameters.get("batch_size")
- opt.epochs = parameters.get("epochs")
+ opt.batch_size = parameters.get('batch_size')
+ opt.epochs = parameters.get('epochs')
device = select_device(opt.device, batch_size=opt.batch_size)
train(hyp_dict, opt, device, callbacks=Callbacks())
-if __name__ == "__main__":
+if __name__ == '__main__':
opt = get_args(known=True)
opt.weights = str(opt.weights)
@@ -99,7 +99,7 @@ def run(parameters, opt):
opt.data = str(opt.data)
opt.project = str(opt.project)
- optimizer_id = os.getenv("COMET_OPTIMIZER_ID")
+ optimizer_id = os.getenv('COMET_OPTIMIZER_ID')
if optimizer_id is None:
with open(opt.comet_optimizer_config) as f:
optimizer_config = json.load(f)
@@ -110,9 +110,9 @@ def run(parameters, opt):
opt.comet_optimizer_id = optimizer.id
status = optimizer.status()
- opt.comet_optimizer_objective = status["spec"]["objective"]
- opt.comet_optimizer_metric = status["spec"]["metric"]
+ opt.comet_optimizer_objective = status['spec']['objective']
+ opt.comet_optimizer_metric = status['spec']['metric']
- logger.info("COMET INFO: Starting Hyperparameter Sweep")
+ logger.info('COMET INFO: Starting Hyperparameter Sweep')
for parameter in optimizer.get_parameters():
- run(parameter["parameters"], opt)
+ run(parameter['parameters'], opt)
diff --git a/utils/loggers/wandb/README.md b/utils/loggers/wandb/README.md
deleted file mode 100644
index d78324b4c8e9..000000000000
--- a/utils/loggers/wandb/README.md
+++ /dev/null
@@ -1,162 +0,0 @@
-📚 This guide explains how to use **Weights & Biases** (W&B) with YOLOv5 🚀. UPDATED 29 September 2021.
-
-- [About Weights & Biases](#about-weights-&-biases)
-- [First-Time Setup](#first-time-setup)
-- [Viewing runs](#viewing-runs)
-- [Disabling wandb](#disabling-wandb)
-- [Advanced Usage: Dataset Versioning and Evaluation](#advanced-usage)
-- [Reports: Share your work with the world!](#reports)
-
-## About Weights & Biases
-
-Think of [W&B](https://wandb.ai/site?utm_campaign=repo_yolo_wandbtutorial) like GitHub for machine learning models. With a few lines of code, save everything you need to debug, compare and reproduce your models — architecture, hyperparameters, git commits, model weights, GPU usage, and even datasets and predictions.
-
-Used by top researchers including teams at OpenAI, Lyft, Github, and MILA, W&B is part of the new standard of best practices for machine learning. How W&B can help you optimize your machine learning workflows:
-
-- [Debug](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#Free-2) model performance in real time
-- [GPU usage](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#System-4) visualized automatically
-- [Custom charts](https://wandb.ai/wandb/customizable-charts/reports/Powerful-Custom-Charts-To-Debug-Model-Peformance--VmlldzoyNzY4ODI) for powerful, extensible visualization
-- [Share insights](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#Share-8) interactively with collaborators
-- [Optimize hyperparameters](https://docs.wandb.com/sweeps) efficiently
-- [Track](https://docs.wandb.com/artifacts) datasets, pipelines, and production models
-
-## First-Time Setup
-
-
- Toggle Details
-When you first train, W&B will prompt you to create a new account and will generate an **API key** for you. If you are an existing user you can retrieve your key from https://wandb.ai/authorize. This key is used to tell W&B where to log your data. You only need to supply your key once, and then it is remembered on the same device.
-
-W&B will create a cloud **project** (default is 'YOLOv5') for your training runs, and each new training run will be provided a unique run **name** within that project as project/name. You can also manually set your project and run name as:
-
-```shell
-$ python train.py --project ... --name ...
-```
-
-YOLOv5 notebook example:
-
-
-
-
-## Viewing Runs
-
-
- Toggle Details
-Run information streams from your environment to the W&B cloud console as you train. This allows you to monitor and even cancel runs in realtime . All important information is logged:
-
-- Training & Validation losses
-- Metrics: Precision, Recall, mAP@0.5, mAP@0.5:0.95
-- Learning Rate over time
-- A bounding box debugging panel, showing the training progress over time
-- GPU: Type, **GPU Utilization**, power, temperature, **CUDA memory usage**
-- System: Disk I/0, CPU utilization, RAM memory usage
-- Your trained model as W&B Artifact
-- Environment: OS and Python types, Git repository and state, **training command**
-
-
-
-
-## Disabling wandb
-
-- training after running `wandb disabled` inside that directory creates no wandb run
- ![Screenshot (84)](https://user-images.githubusercontent.com/15766192/143441777-c780bdd7-7cb4-4404-9559-b4316030a985.png)
-
-- To enable wandb again, run `wandb online`
- ![Screenshot (85)](https://user-images.githubusercontent.com/15766192/143441866-7191b2cb-22f0-4e0f-ae64-2dc47dc13078.png)
-
-## Advanced Usage
-
-You can leverage W&B artifacts and Tables integration to easily visualize and manage your datasets, models and training evaluations. Here are some quick examples to get you started.
-
-
- 1: Train and Log Evaluation simultaneousy
- This is an extension of the previous section, but it'll also training after uploading the dataset. This also evaluation Table
- Evaluation table compares your predictions and ground truths across the validation set for each epoch. It uses the references to the already uploaded datasets,
- so no images will be uploaded from your system more than once.
-
- Usage
- Code $ python train.py --upload_data val
-
-![Screenshot from 2021-11-21 17-40-06](https://user-images.githubusercontent.com/15766192/142761183-c1696d8c-3f38-45ab-991a-bb0dfd98ae7d.png)
-
-
-
-2. Visualize and Version Datasets
- Log, visualize, dynamically query, and understand your data with W&B Tables. You can use the following command to log your dataset as a W&B Table. This will generate a {dataset}_wandb.yaml
file which can be used to train from dataset artifact.
-
- Usage
- Code $ python utils/logger/wandb/log_dataset.py --project ... --name ... --data ..
-
-![Screenshot (64)](https://user-images.githubusercontent.com/15766192/128486078-d8433890-98a3-4d12-8986-b6c0e3fc64b9.png)
-
-
-
- 3: Train using dataset artifact
- When you upload a dataset as described in the first section, you get a new config file with an added `_wandb` to its name. This file contains the information that
- can be used to train a model directly from the dataset artifact. This also logs evaluation
-
- Usage
- Code $ python train.py --data {data}_wandb.yaml
-
-![Screenshot (72)](https://user-images.githubusercontent.com/15766192/128979739-4cf63aeb-a76f-483f-8861-1c0100b938a5.png)
-
-
-
- 4: Save model checkpoints as artifacts
- To enable saving and versioning checkpoints of your experiment, pass `--save_period n` with the base cammand, where `n` represents checkpoint interval.
- You can also log both the dataset and model checkpoints simultaneously. If not passed, only the final model will be logged
-
-
- Usage
- Code $ python train.py --save_period 1
-
-![Screenshot (68)](https://user-images.githubusercontent.com/15766192/128726138-ec6c1f60-639d-437d-b4ee-3acd9de47ef3.png)
-
-
-
-
-
- 5: Resume runs from checkpoint artifacts.
-Any run can be resumed using artifacts if the --resume
argument starts with wandb-artifact://
prefix followed by the run path, i.e, wandb-artifact://username/project/runid
. This doesn't require the model checkpoint to be present on the local system.
-
-
- Usage
- Code $ python train.py --resume wandb-artifact://{run_path}
-
-![Screenshot (70)](https://user-images.githubusercontent.com/15766192/128728988-4e84b355-6c87-41ae-a591-14aecf45343e.png)
-
-
-
- 6: Resume runs from dataset artifact & checkpoint artifacts.
- Local dataset or model checkpoints are not required. This can be used to resume runs directly on a different device
- The syntax is same as the previous section, but you'll need to lof both the dataset and model checkpoints as artifacts, i.e, set bot --upload_dataset
or
- train from _wandb.yaml
file and set --save_period
-
-
- Usage
- Code $ python train.py --resume wandb-artifact://{run_path}
-
-![Screenshot (70)](https://user-images.githubusercontent.com/15766192/128728988-4e84b355-6c87-41ae-a591-14aecf45343e.png)
-
-
-
-
-
- Reports
-W&B Reports can be created from your saved runs for sharing online. Once a report is created you will receive a link you can use to publically share your results. Here is an example report created from the COCO128 tutorial trainings of all four YOLOv5 models ([link](https://wandb.ai/glenn-jocher/yolov5_tutorial/reports/YOLOv5-COCO128-Tutorial-Results--VmlldzozMDI5OTY)).
-
-
-
-## Environments
-
-YOLOv5 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled):
-
-- **Google Colab and Kaggle** notebooks with free GPU:
-- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/GCP-Quickstart)
-- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/AWS-Quickstart)
-- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/Docker-Quickstart)
-
-## Status
-
-![CI CPU testing](https://github.com/ultralytics/yolov5/workflows/CI%20CPU%20testing/badge.svg)
-
-If this badge is green, all [YOLOv5 GitHub Actions](https://github.com/ultralytics/yolov5/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv5 training ([train.py](https://github.com/ultralytics/yolov5/blob/master/train.py)), validation ([val.py](https://github.com/ultralytics/yolov5/blob/master/val.py)), inference ([detect.py](https://github.com/ultralytics/yolov5/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov5/blob/master/export.py)) on macOS, Windows, and Ubuntu every 24 hours and on every commit.
diff --git a/utils/loggers/wandb/log_dataset.py b/utils/loggers/wandb/log_dataset.py
deleted file mode 100644
index 06e81fb69307..000000000000
--- a/utils/loggers/wandb/log_dataset.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import argparse
-
-from wandb_utils import WandbLogger
-
-from utils.general import LOGGER
-
-WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
-
-
-def create_dataset_artifact(opt):
- logger = WandbLogger(opt, None, job_type='Dataset Creation') # TODO: return value unused
- if not logger.wandb:
- LOGGER.info("install wandb using `pip install wandb` to log the dataset")
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('--data', type=str, default='data/coco128.yaml', help='data.yaml path')
- parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset')
- parser.add_argument('--project', type=str, default='YOLOv5', help='name of W&B Project')
- parser.add_argument('--entity', default=None, help='W&B entity')
- parser.add_argument('--name', type=str, default='log dataset', help='name of W&B run')
-
- opt = parser.parse_args()
- opt.resume = False # Explicitly disallow resume check for dataset upload job
-
- create_dataset_artifact(opt)
diff --git a/utils/loggers/wandb/sweep.py b/utils/loggers/wandb/sweep.py
deleted file mode 100644
index d49ea6f2778b..000000000000
--- a/utils/loggers/wandb/sweep.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import sys
-from pathlib import Path
-
-import wandb
-
-FILE = Path(__file__).resolve()
-ROOT = FILE.parents[3] # YOLOv5 root directory
-if str(ROOT) not in sys.path:
- sys.path.append(str(ROOT)) # add ROOT to PATH
-
-from train import parse_opt, train
-from utils.callbacks import Callbacks
-from utils.general import increment_path
-from utils.torch_utils import select_device
-
-
-def sweep():
- wandb.init()
- # Get hyp dict from sweep agent. Copy because train() modifies parameters which confused wandb.
- hyp_dict = vars(wandb.config).get("_items").copy()
-
- # Workaround: get necessary opt args
- opt = parse_opt(known=True)
- opt.batch_size = hyp_dict.get("batch_size")
- opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve))
- opt.epochs = hyp_dict.get("epochs")
- opt.nosave = True
- opt.data = hyp_dict.get("data")
- opt.weights = str(opt.weights)
- opt.cfg = str(opt.cfg)
- opt.data = str(opt.data)
- opt.hyp = str(opt.hyp)
- opt.project = str(opt.project)
- device = select_device(opt.device, batch_size=opt.batch_size)
-
- # train
- train(hyp_dict, opt, device, callbacks=Callbacks())
-
-
-if __name__ == "__main__":
- sweep()
diff --git a/utils/loggers/wandb/sweep.yaml b/utils/loggers/wandb/sweep.yaml
deleted file mode 100644
index 688b1ea0285f..000000000000
--- a/utils/loggers/wandb/sweep.yaml
+++ /dev/null
@@ -1,143 +0,0 @@
-# Hyperparameters for training
-# To set range-
-# Provide min and max values as:
-# parameter:
-#
-# min: scalar
-# max: scalar
-# OR
-#
-# Set a specific list of search space-
-# parameter:
-# values: [scalar1, scalar2, scalar3...]
-#
-# You can use grid, bayesian and hyperopt search strategy
-# For more info on configuring sweeps visit - https://docs.wandb.ai/guides/sweeps/configuration
-
-program: utils/loggers/wandb/sweep.py
-method: random
-metric:
- name: metrics/mAP_0.5
- goal: maximize
-
-parameters:
- # hyperparameters: set either min, max range or values list
- data:
- value: "data/coco128.yaml"
- batch_size:
- values: [64]
- epochs:
- values: [10]
-
- lr0:
- distribution: uniform
- min: 1e-5
- max: 1e-1
- lrf:
- distribution: uniform
- min: 0.01
- max: 1.0
- momentum:
- distribution: uniform
- min: 0.6
- max: 0.98
- weight_decay:
- distribution: uniform
- min: 0.0
- max: 0.001
- warmup_epochs:
- distribution: uniform
- min: 0.0
- max: 5.0
- warmup_momentum:
- distribution: uniform
- min: 0.0
- max: 0.95
- warmup_bias_lr:
- distribution: uniform
- min: 0.0
- max: 0.2
- box:
- distribution: uniform
- min: 0.02
- max: 0.2
- cls:
- distribution: uniform
- min: 0.2
- max: 4.0
- cls_pw:
- distribution: uniform
- min: 0.5
- max: 2.0
- obj:
- distribution: uniform
- min: 0.2
- max: 4.0
- obj_pw:
- distribution: uniform
- min: 0.5
- max: 2.0
- iou_t:
- distribution: uniform
- min: 0.1
- max: 0.7
- anchor_t:
- distribution: uniform
- min: 2.0
- max: 8.0
- fl_gamma:
- distribution: uniform
- min: 0.0
- max: 4.0
- hsv_h:
- distribution: uniform
- min: 0.0
- max: 0.1
- hsv_s:
- distribution: uniform
- min: 0.0
- max: 0.9
- hsv_v:
- distribution: uniform
- min: 0.0
- max: 0.9
- degrees:
- distribution: uniform
- min: 0.0
- max: 45.0
- translate:
- distribution: uniform
- min: 0.0
- max: 0.9
- scale:
- distribution: uniform
- min: 0.0
- max: 0.9
- shear:
- distribution: uniform
- min: 0.0
- max: 10.0
- perspective:
- distribution: uniform
- min: 0.0
- max: 0.001
- flipud:
- distribution: uniform
- min: 0.0
- max: 1.0
- fliplr:
- distribution: uniform
- min: 0.0
- max: 1.0
- mosaic:
- distribution: uniform
- min: 0.0
- max: 1.0
- mixup:
- distribution: uniform
- min: 0.0
- max: 1.0
- copy_paste:
- distribution: uniform
- min: 0.0
- max: 1.0
diff --git a/utils/loggers/wandb/wandb_utils.py b/utils/loggers/wandb/wandb_utils.py
index e850d2ac8a7c..c8ab38197381 100644
--- a/utils/loggers/wandb/wandb_utils.py
+++ b/utils/loggers/wandb/wandb_utils.py
@@ -1,110 +1,32 @@
-"""Utilities and tools for tracking runs with Weights & Biases."""
+# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
+
+# WARNING ⚠️ wandb is deprecated and will be removed in future release.
+# See supported integrations at https://github.com/ultralytics/yolov5#integrations
import logging
import os
import sys
from contextlib import contextmanager
from pathlib import Path
-from typing import Dict
-import yaml
-from tqdm import tqdm
+from utils.general import LOGGER, colorstr
FILE = Path(__file__).resolve()
ROOT = FILE.parents[3] # YOLOv5 root directory
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
-
-from utils.dataloaders import LoadImagesAndLabels, img2label_paths
-from utils.general import LOGGER, check_dataset, check_file
+RANK = int(os.getenv('RANK', -1))
+DEPRECATION_WARNING = f"{colorstr('wandb')}: WARNING ⚠️ wandb is deprecated and will be removed in a future release. " \
+ f'See supported integrations at https://github.com/ultralytics/yolov5#integrations.'
try:
import wandb
assert hasattr(wandb, '__version__') # verify package import not local dir
+ LOGGER.warning(DEPRECATION_WARNING)
except (ImportError, AssertionError):
wandb = None
-RANK = int(os.getenv('RANK', -1))
-WANDB_ARTIFACT_PREFIX = 'wandb-artifact://'
-
-
-def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX):
- return from_string[len(prefix):]
-
-
-def check_wandb_config_file(data_config_file):
- wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path
- if Path(wandb_config).is_file():
- return wandb_config
- return data_config_file
-
-
-def check_wandb_dataset(data_file):
- is_trainset_wandb_artifact = False
- is_valset_wandb_artifact = False
- if isinstance(data_file, dict):
- # In that case another dataset manager has already processed it and we don't have to
- return data_file
- if check_file(data_file) and data_file.endswith('.yaml'):
- with open(data_file, errors='ignore') as f:
- data_dict = yaml.safe_load(f)
- is_trainset_wandb_artifact = isinstance(data_dict['train'],
- str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX)
- is_valset_wandb_artifact = isinstance(data_dict['val'],
- str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX)
- if is_trainset_wandb_artifact or is_valset_wandb_artifact:
- return data_dict
- else:
- return check_dataset(data_file)
-
-
-def get_run_info(run_path):
- run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX))
- run_id = run_path.stem
- project = run_path.parent.stem
- entity = run_path.parent.parent.stem
- model_artifact_name = 'run_' + run_id + '_model'
- return entity, project, run_id, model_artifact_name
-
-
-def check_wandb_resume(opt):
- process_wandb_config_ddp_mode(opt) if RANK not in [-1, 0] else None
- if isinstance(opt.resume, str):
- if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
- if RANK not in [-1, 0]: # For resuming DDP runs
- entity, project, run_id, model_artifact_name = get_run_info(opt.resume)
- api = wandb.Api()
- artifact = api.artifact(entity + '/' + project + '/' + model_artifact_name + ':latest')
- modeldir = artifact.download()
- opt.weights = str(Path(modeldir) / "last.pt")
- return True
- return None
-
-
-def process_wandb_config_ddp_mode(opt):
- with open(check_file(opt.data), errors='ignore') as f:
- data_dict = yaml.safe_load(f) # data dict
- train_dir, val_dir = None, None
- if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX):
- api = wandb.Api()
- train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias)
- train_dir = train_artifact.download()
- train_path = Path(train_dir) / 'data/images/'
- data_dict['train'] = str(train_path)
-
- if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX):
- api = wandb.Api()
- val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias)
- val_dir = val_artifact.download()
- val_path = Path(val_dir) / 'data/images/'
- data_dict['val'] = str(val_path)
- if train_dir or val_dir:
- ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml')
- with open(ddp_data_path, 'w') as f:
- yaml.safe_dump(data_dict, f)
- opt.data = ddp_data_path
-
class WandbLogger():
"""Log training runs, datasets, models, and predictions to Weights & Biases.
@@ -134,84 +56,31 @@ def __init__(self, opt, run_id=None, job_type='Training'):
"""
# Pre-training routine --
self.job_type = job_type
- self.wandb, self.wandb_run = wandb, None if not wandb else wandb.run
+ self.wandb, self.wandb_run = wandb, wandb.run if wandb else None
self.val_artifact, self.train_artifact = None, None
self.train_artifact_path, self.val_artifact_path = None, None
self.result_artifact = None
self.val_table, self.result_table = None, None
- self.bbox_media_panel_images = []
- self.val_table_path_map = None
self.max_imgs_to_log = 16
- self.wandb_artifact_data_dict = None
self.data_dict = None
- # It's more elegant to stick to 1 wandb.init call,
- # but useful config data is overwritten in the WandbLogger's wandb.init call
- if isinstance(opt.resume, str): # checks resume from artifact
- if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
- entity, project, run_id, model_artifact_name = get_run_info(opt.resume)
- model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name
- assert wandb, 'install wandb to resume wandb runs'
- # Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config
- self.wandb_run = wandb.init(id=run_id,
- project=project,
- entity=entity,
- resume='allow',
- allow_val_change=True)
- opt.resume = model_artifact_name
- elif self.wandb:
+ if self.wandb:
self.wandb_run = wandb.init(config=opt,
- resume="allow",
+ resume='allow',
project='YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem,
entity=opt.entity,
name=opt.name if opt.name != 'exp' else None,
job_type=job_type,
id=run_id,
allow_val_change=True) if not wandb.run else wandb.run
+
if self.wandb_run:
if self.job_type == 'Training':
- if opt.upload_dataset:
- if not opt.resume:
- self.wandb_artifact_data_dict = self.check_and_upload_dataset(opt)
-
if isinstance(opt.data, dict):
# This means another dataset manager has already processed the dataset info (e.g. ClearML)
# and they will have stored the already processed dict in opt.data
self.data_dict = opt.data
- elif opt.resume:
- # resume from artifact
- if isinstance(opt.resume, str) and opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
- self.data_dict = dict(self.wandb_run.config.data_dict)
- else: # local resume
- self.data_dict = check_wandb_dataset(opt.data)
- else:
- self.data_dict = check_wandb_dataset(opt.data)
- self.wandb_artifact_data_dict = self.wandb_artifact_data_dict or self.data_dict
-
- # write data_dict to config. useful for resuming from artifacts. Do this only when not resuming.
- self.wandb_run.config.update({'data_dict': self.wandb_artifact_data_dict}, allow_val_change=True)
self.setup_training(opt)
- if self.job_type == 'Dataset Creation':
- self.wandb_run.config.update({"upload_dataset": True})
- self.data_dict = self.check_and_upload_dataset(opt)
-
- def check_and_upload_dataset(self, opt):
- """
- Check if the dataset format is compatible and upload it as W&B artifact
-
- arguments:
- opt (namespace)-- Commandline arguments for current run
-
- returns:
- Updated dataset info dictionary where local dataset paths are replaced by WAND_ARFACT_PREFIX links.
- """
- assert wandb, 'Install wandb to upload dataset'
- config_path = self.log_dataset_artifact(opt.data, opt.single_cls,
- 'YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem)
- with open(config_path, errors='ignore') as f:
- wandb_data_dict = yaml.safe_load(f)
- return wandb_data_dict
-
def setup_training(self, opt):
"""
Setup the necessary processes for training YOLO models:
@@ -226,81 +95,18 @@ def setup_training(self, opt):
self.log_dict, self.current_epoch = {}, 0
self.bbox_interval = opt.bbox_interval
if isinstance(opt.resume, str):
- modeldir, _ = self.download_model_artifact(opt)
- if modeldir:
- self.weights = Path(modeldir) / "last.pt"
+ model_dir, _ = self.download_model_artifact(opt)
+ if model_dir:
+ self.weights = Path(model_dir) / 'last.pt'
config = self.wandb_run.config
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp, opt.imgsz = str(
- self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs,\
+ self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \
config.hyp, config.imgsz
- data_dict = self.data_dict
- if self.val_artifact is None: # If --upload_dataset is set, use the existing artifact, don't download
- self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(
- data_dict.get('train'), opt.artifact_alias)
- self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(
- data_dict.get('val'), opt.artifact_alias)
-
- if self.train_artifact_path is not None:
- train_path = Path(self.train_artifact_path) / 'data/images/'
- data_dict['train'] = str(train_path)
- if self.val_artifact_path is not None:
- val_path = Path(self.val_artifact_path) / 'data/images/'
- data_dict['val'] = str(val_path)
- if self.val_artifact is not None:
- self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
- columns = ["epoch", "id", "ground truth", "prediction"]
- columns.extend(self.data_dict['names'])
- self.result_table = wandb.Table(columns)
- self.val_table = self.val_artifact.get("val")
- if self.val_table_path_map is None:
- self.map_val_table_path()
if opt.bbox_interval == -1:
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
if opt.evolve or opt.noplots:
self.bbox_interval = opt.bbox_interval = opt.epochs + 1 # disable bbox_interval
- train_from_artifact = self.train_artifact_path is not None and self.val_artifact_path is not None
- # Update the the data_dict to point to local artifacts dir
- if train_from_artifact:
- self.data_dict = data_dict
-
- def download_dataset_artifact(self, path, alias):
- """
- download the model checkpoint artifact if the path starts with WANDB_ARTIFACT_PREFIX
-
- arguments:
- path -- path of the dataset to be used for training
- alias (str)-- alias of the artifact to be download/used for training
-
- returns:
- (str, wandb.Artifact) -- path of the downladed dataset and it's corresponding artifact object if dataset
- is found otherwise returns (None, None)
- """
- if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
- artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
- dataset_artifact = wandb.use_artifact(artifact_path.as_posix().replace("\\", "/"))
- assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
- datadir = dataset_artifact.download()
- return datadir, dataset_artifact
- return None, None
-
- def download_model_artifact(self, opt):
- """
- download the model checkpoint artifact if the resume path starts with WANDB_ARTIFACT_PREFIX
-
- arguments:
- opt (namespace) -- Commandline arguments for this run
- """
- if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
- model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest")
- assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist'
- modeldir = model_artifact.download()
- # epochs_trained = model_artifact.metadata.get('epochs_trained')
- total_epochs = model_artifact.metadata.get('total_epochs')
- is_finished = total_epochs is None
- assert not is_finished, 'training is finished, can only resume incomplete runs.'
- return modeldir, model_artifact
- return None, None
def log_model(self, path, opt, epoch, fitness_score, best_model=False):
"""
@@ -325,192 +131,10 @@ def log_model(self, path, opt, epoch, fitness_score, best_model=False):
model_artifact.add_file(str(path / 'last.pt'), name='last.pt')
wandb.log_artifact(model_artifact,
aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else ''])
- LOGGER.info(f"Saving model artifact on epoch {epoch + 1}")
-
- def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False):
- """
- Log the dataset as W&B artifact and return the new data file with W&B links
-
- arguments:
- data_file (str) -- the .yaml file with information about the dataset like - path, classes etc.
- single_class (boolean) -- train multi-class data as single-class
- project (str) -- project name. Used to construct the artifact path
- overwrite_config (boolean) -- overwrites the data.yaml file if set to true otherwise creates a new
- file with _wandb postfix. Eg -> data_wandb.yaml
-
- returns:
- the new .yaml file with artifact links. it can be used to start training directly from artifacts
- """
- upload_dataset = self.wandb_run.config.upload_dataset
- log_val_only = isinstance(upload_dataset, str) and upload_dataset == 'val'
- self.data_dict = check_dataset(data_file) # parse and check
- data = dict(self.data_dict)
- nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names'])
- names = {k: v for k, v in enumerate(names)} # to index dictionary
-
- # log train set
- if not log_val_only:
- self.train_artifact = self.create_dataset_table(LoadImagesAndLabels(data['train'], rect=True, batch_size=1),
- names,
- name='train') if data.get('train') else None
- if data.get('train'):
- data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train')
-
- self.val_artifact = self.create_dataset_table(
- LoadImagesAndLabels(data['val'], rect=True, batch_size=1), names, name='val') if data.get('val') else None
- if data.get('val'):
- data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val')
-
- path = Path(data_file)
- # create a _wandb.yaml file with artifacts links if both train and test set are logged
- if not log_val_only:
- path = (path.stem if overwrite_config else path.stem + '_wandb') + '.yaml' # updated data.yaml path
- path = ROOT / 'data' / path
- data.pop('download', None)
- data.pop('path', None)
- with open(path, 'w') as f:
- yaml.safe_dump(data, f)
- LOGGER.info(f"Created dataset config file {path}")
-
- if self.job_type == 'Training': # builds correct artifact pipeline graph
- if not log_val_only:
- self.wandb_run.log_artifact(
- self.train_artifact) # calling use_artifact downloads the dataset. NOT NEEDED!
- self.wandb_run.use_artifact(self.val_artifact)
- self.val_artifact.wait()
- self.val_table = self.val_artifact.get('val')
- self.map_val_table_path()
- else:
- self.wandb_run.log_artifact(self.train_artifact)
- self.wandb_run.log_artifact(self.val_artifact)
- return path
-
- def map_val_table_path(self):
- """
- Map the validation dataset Table like name of file -> it's id in the W&B Table.
- Useful for - referencing artifacts for evaluation.
- """
- self.val_table_path_map = {}
- LOGGER.info("Mapping dataset")
- for i, data in enumerate(tqdm(self.val_table.data)):
- self.val_table_path_map[data[3]] = data[0]
-
- def create_dataset_table(self, dataset: LoadImagesAndLabels, class_to_id: Dict[int, str], name: str = 'dataset'):
- """
- Create and return W&B artifact containing W&B Table of the dataset.
-
- arguments:
- dataset -- instance of LoadImagesAndLabels class used to iterate over the data to build Table
- class_to_id -- hash map that maps class ids to labels
- name -- name of the artifact
-
- returns:
- dataset artifact to be logged or used
- """
- # TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
- artifact = wandb.Artifact(name=name, type="dataset")
- img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None
- img_files = tqdm(dataset.im_files) if not img_files else img_files
- for img_file in img_files:
- if Path(img_file).is_dir():
- artifact.add_dir(img_file, name='data/images')
- labels_path = 'labels'.join(dataset.path.rsplit('images', 1))
- artifact.add_dir(labels_path, name='data/labels')
- else:
- artifact.add_file(img_file, name='data/images/' + Path(img_file).name)
- label_file = Path(img2label_paths([img_file])[0])
- artifact.add_file(str(label_file), name='data/labels/' +
- label_file.name) if label_file.exists() else None
- table = wandb.Table(columns=["id", "train_image", "Classes", "name"])
- class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()])
- for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)):
- box_data, img_classes = [], {}
- for cls, *xywh in labels[:, 1:].tolist():
- cls = int(cls)
- box_data.append({
- "position": {
- "middle": [xywh[0], xywh[1]],
- "width": xywh[2],
- "height": xywh[3]},
- "class_id": cls,
- "box_caption": "%s" % (class_to_id[cls])})
- img_classes[cls] = class_to_id[cls]
- boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space
- table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), list(img_classes.values()),
- Path(paths).name)
- artifact.add(table, name)
- return artifact
-
- def log_training_progress(self, predn, path, names):
- """
- Build evaluation Table. Uses reference from validation dataset table.
-
- arguments:
- predn (list): list of predictions in the native space in the format - [xmin, ymin, xmax, ymax, confidence, class]
- path (str): local path of the current evaluation image
- names (dict(int, str)): hash map that maps class ids to labels
- """
- class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
- box_data = []
- avg_conf_per_class = [0] * len(self.data_dict['names'])
- pred_class_count = {}
- for *xyxy, conf, cls in predn.tolist():
- if conf >= 0.25:
- cls = int(cls)
- box_data.append({
- "position": {
- "minX": xyxy[0],
- "minY": xyxy[1],
- "maxX": xyxy[2],
- "maxY": xyxy[3]},
- "class_id": cls,
- "box_caption": f"{names[cls]} {conf:.3f}",
- "scores": {
- "class_score": conf},
- "domain": "pixel"})
- avg_conf_per_class[cls] += conf
-
- if cls in pred_class_count:
- pred_class_count[cls] += 1
- else:
- pred_class_count[cls] = 1
-
- for pred_class in pred_class_count.keys():
- avg_conf_per_class[pred_class] = avg_conf_per_class[pred_class] / pred_class_count[pred_class]
-
- boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
- id = self.val_table_path_map[Path(path).name]
- self.result_table.add_data(self.current_epoch, id, self.val_table.data[id][1],
- wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set),
- *avg_conf_per_class)
+ LOGGER.info(f'Saving model artifact on epoch {epoch + 1}')
def val_one_image(self, pred, predn, path, names, im):
- """
- Log validation data for one image. updates the result Table if validation dataset is uploaded and log bbox media panel
-
- arguments:
- pred (list): list of scaled predictions in the format - [xmin, ymin, xmax, ymax, confidence, class]
- predn (list): list of predictions in the native space - [xmin, ymin, xmax, ymax, confidence, class]
- path (str): local path of the current evaluation image
- """
- if self.val_table and self.result_table: # Log Table if Val dataset is uploaded as artifact
- self.log_training_progress(predn, path, names)
-
- if len(self.bbox_media_panel_images) < self.max_imgs_to_log and self.current_epoch > 0:
- if self.current_epoch % self.bbox_interval == 0:
- box_data = [{
- "position": {
- "minX": xyxy[0],
- "minY": xyxy[1],
- "maxX": xyxy[2],
- "maxY": xyxy[3]},
- "class_id": int(cls),
- "box_caption": f"{names[int(cls)]} {conf:.3f}",
- "scores": {
- "class_score": conf},
- "domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
- boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
- self.bbox_media_panel_images.append(wandb.Image(im, boxes=boxes, caption=path.name))
+ pass
def log(self, log_dict):
"""
@@ -523,7 +147,7 @@ def log(self, log_dict):
for key, value in log_dict.items():
self.log_dict[key] = value
- def end_epoch(self, best_result=False):
+ def end_epoch(self):
"""
commit the log_dict, model artifacts and Tables to W&B and flush the log_dict.
@@ -532,31 +156,15 @@ def end_epoch(self, best_result=False):
"""
if self.wandb_run:
with all_logging_disabled():
- if self.bbox_media_panel_images:
- self.log_dict["BoundingBoxDebugger"] = self.bbox_media_panel_images
try:
wandb.log(self.log_dict)
except BaseException as e:
LOGGER.info(
- f"An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}"
+ f'An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}'
)
self.wandb_run.finish()
self.wandb_run = None
-
self.log_dict = {}
- self.bbox_media_panel_images = []
- if self.result_artifact:
- self.result_artifact.add(self.result_table, 'result')
- wandb.log_artifact(self.result_artifact,
- aliases=[
- 'latest', 'last', 'epoch ' + str(self.current_epoch),
- ('best' if best_result else '')])
-
- wandb.log({"evaluation": self.result_table})
- columns = ["epoch", "id", "ground truth", "prediction"]
- columns.extend(self.data_dict['names'])
- self.result_table = wandb.Table(columns)
- self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
def finish_run(self):
"""
@@ -567,6 +175,7 @@ def finish_run(self):
with all_logging_disabled():
wandb.log(self.log_dict)
wandb.run.finish()
+ LOGGER.warning(DEPRECATION_WARNING)
@contextmanager
diff --git a/utils/metrics.py b/utils/metrics.py
index ee7d33982cfc..95f364c23f34 100644
--- a/utils/metrics.py
+++ b/utils/metrics.py
@@ -28,7 +28,7 @@ def smooth(y, f=0.05):
return np.convolve(yp, np.ones(nf) / nf, mode='valid') # y-smoothed
-def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16):
+def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16, prefix=''):
""" Compute the average precision, given the recall and precision curves.
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
# Arguments
@@ -83,10 +83,10 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names
names = [v for k, v in names.items() if k in unique_classes] # list: only classes that have data
names = dict(enumerate(names)) # to dict
if plot:
- plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names)
- plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1')
- plot_mc_curve(px, p, Path(save_dir) / 'P_curve.png', names, ylabel='Precision')
- plot_mc_curve(px, r, Path(save_dir) / 'R_curve.png', names, ylabel='Recall')
+ plot_pr_curve(px, py, ap, Path(save_dir) / f'{prefix}PR_curve.png', names)
+ plot_mc_curve(px, f1, Path(save_dir) / f'{prefix}F1_curve.png', names, ylabel='F1')
+ plot_mc_curve(px, p, Path(save_dir) / f'{prefix}P_curve.png', names, ylabel='Precision')
+ plot_mc_curve(px, r, Path(save_dir) / f'{prefix}R_curve.png', names, ylabel='Recall')
i = smooth(f1.mean(0), 0.1).argmax() # max F1 index
p, r, f1 = p[:, i], r[:, i], f1[:, i]
@@ -170,15 +170,12 @@ def process_batch(self, detections, labels):
if n and sum(j) == 1:
self.matrix[detection_classes[m1[j]], gc] += 1 # correct
else:
- self.matrix[self.nc, gc] += 1 # background FP
+ self.matrix[self.nc, gc] += 1 # true background
if n:
for i, dc in enumerate(detection_classes):
if not any(m1 == i):
- self.matrix[dc, self.nc] += 1 # background FN
-
- def matrix(self):
- return self.matrix
+ self.matrix[dc, self.nc] += 1 # predicted background
def tp_fp(self):
tp = self.matrix.diagonal() # true positives
@@ -186,7 +183,7 @@ def tp_fp(self):
# fn = self.matrix.sum(0) - tp # false negatives (missed detections)
return tp[:-1], fp[:-1] # remove background class
- @TryExcept('WARNING: ConfusionMatrix plot failure: ')
+ @TryExcept('WARNING ⚠️ ConfusionMatrix plot failure')
def plot(self, normalize=True, save_dir='', names=()):
import seaborn as sn
@@ -197,20 +194,21 @@ def plot(self, normalize=True, save_dir='', names=()):
nc, nn = self.nc, len(names) # number of classes, names
sn.set(font_scale=1.0 if nc < 50 else 0.8) # for label size
labels = (0 < nn < 99) and (nn == nc) # apply names to ticklabels
+ ticklabels = (names + ['background']) if labels else 'auto'
with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress empty matrix RuntimeWarning: All-NaN slice encountered
sn.heatmap(array,
ax=ax,
annot=nc < 30,
annot_kws={
- "size": 8},
+ 'size': 8},
cmap='Blues',
fmt='.2f',
square=True,
vmin=0.0,
- xticklabels=names + ['background FP'] if labels else "auto",
- yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1))
- ax.set_ylabel('True')
+ xticklabels=ticklabels,
+ yticklabels=ticklabels).set_facecolor((1, 1, 1))
+ ax.set_xlabel('True')
ax.set_ylabel('Predicted')
ax.set_title('Confusion Matrix')
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250)
@@ -226,19 +224,19 @@ def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7
# Get the coordinates of bounding boxes
if xywh: # transform from xywh to xyxy
- (x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, 1), box2.chunk(4, 1)
+ (x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)
w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_
b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_
else: # x1, y1, x2, y2 = box1
- b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, 1)
- b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, 1)
- w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1
- w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1
+ b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)
+ b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)
+ w1, h1 = b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)
+ w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)
# Intersection area
- inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
- (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
+ inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) * \
+ (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0)
# Union Area
union = w1 * h1 + w2 * h2 - inter + eps
@@ -246,13 +244,13 @@ def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7
# IoU
iou = inter / union
if CIoU or DIoU or GIoU:
- cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width
- ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height
+ cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width
+ ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared
rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center dist ** 2
if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
- v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / (h2 + eps)) - torch.atan(w1 / (h1 + eps)), 2)
+ v = (4 / math.pi ** 2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2)
with torch.no_grad():
alpha = v / (v - iou + (1 + eps))
return iou - (rho2 / c2 + v * alpha) # CIoU
@@ -262,11 +260,6 @@ def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7
return iou # IoU
-def box_area(box):
- # box = xyxy(4,n)
- return (box[2] - box[0]) * (box[3] - box[1])
-
-
def box_iou(box1, box2, eps=1e-7):
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
"""
@@ -281,11 +274,11 @@ def box_iou(box1, box2, eps=1e-7):
"""
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
- (a1, a2), (b1, b2) = box1[:, None].chunk(2, 2), box2.chunk(2, 1)
+ (a1, a2), (b1, b2) = box1.unsqueeze(1).chunk(2, 2), box2.unsqueeze(0).chunk(2, 2)
inter = (torch.min(a2, b2) - torch.max(a1, b1)).clamp(0).prod(2)
# IoU = inter / (area1 + area2 - inter)
- return inter / (box_area(box1.T)[:, None] + box_area(box2.T) - inter + eps)
+ return inter / ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter + eps)
def bbox_ioa(box1, box2, eps=1e-7):
@@ -338,7 +331,7 @@ def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()):
ax.set_ylabel('Precision')
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
- ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
+ ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left')
ax.set_title('Precision-Recall Curve')
fig.savefig(save_dir, dpi=250)
plt.close(fig)
@@ -361,7 +354,7 @@ def plot_mc_curve(px, py, save_dir=Path('mc_curve.png'), names=(), xlabel='Confi
ax.set_ylabel(ylabel)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
- ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
+ ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left')
ax.set_title(f'{ylabel}-Confidence Curve')
fig.savefig(save_dir, dpi=250)
plt.close(fig)
diff --git a/utils/plots.py b/utils/plots.py
index 0530d0abdf48..24c618c80b59 100644
--- a/utils/plots.py
+++ b/utils/plots.py
@@ -20,9 +20,10 @@
from PIL import Image, ImageDraw, ImageFont
from utils import TryExcept, threaded
-from utils.general import (CONFIG_DIR, FONT, LOGGER, check_font, check_requirements, clip_coords, increment_path,
+from utils.general import (CONFIG_DIR, FONT, LOGGER, check_font, check_requirements, clip_boxes, increment_path,
is_ascii, xywh2xyxy, xyxy2xywh)
from utils.metrics import fitness
+from utils.segment.general import scale_image
# Settings
RANK = int(os.getenv('RANK', -1))
@@ -87,7 +88,8 @@ def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 2
if self.pil or not is_ascii(label):
self.draw.rectangle(box, width=self.lw, outline=color) # box
if label:
- w, h = self.font.getsize(label) # text width, height
+ w, h = self.font.getsize(label) # text width, height (WARNING: deprecated) in 9.2.0
+ # _, _, w, h = self.font.getbbox(label) # text width, height (New)
outside = box[1] - h >= 0 # label fits outside box
self.draw.rectangle(
(box[0], box[1] - h if outside else box[1], box[0] + w + 1,
@@ -113,6 +115,36 @@ def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 2
thickness=tf,
lineType=cv2.LINE_AA)
+ def masks(self, masks, colors, im_gpu, alpha=0.5, retina_masks=False):
+ """Plot masks at once.
+ Args:
+ masks (tensor): predicted masks on cuda, shape: [n, h, w]
+ colors (List[List[Int]]): colors for predicted masks, [[r, g, b] * n]
+ im_gpu (tensor): img is in cuda, shape: [3, h, w], range: [0, 1]
+ alpha (float): mask transparency: 0.0 fully transparent, 1.0 opaque
+ """
+ if self.pil:
+ # convert to numpy first
+ self.im = np.asarray(self.im).copy()
+ if len(masks) == 0:
+ self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255
+ colors = torch.tensor(colors, device=im_gpu.device, dtype=torch.float32) / 255.0
+ colors = colors[:, None, None] # shape(n,1,1,3)
+ masks = masks.unsqueeze(3) # shape(n,h,w,1)
+ masks_color = masks * (colors * alpha) # shape(n,h,w,3)
+
+ inv_alph_masks = (1 - masks * alpha).cumprod(0) # shape(n,h,w,1)
+ mcs = (masks_color * inv_alph_masks).sum(0) * 2 # mask color summand shape(n,h,w,3)
+
+ im_gpu = im_gpu.flip(dims=[0]) # flip channel
+ im_gpu = im_gpu.permute(1, 2, 0).contiguous() # shape(h,w,3)
+ im_gpu = im_gpu * inv_alph_masks[-1] + mcs
+ im_mask = (im_gpu * 255).byte().cpu().numpy()
+ self.im[:] = im_mask if retina_masks else scale_image(im_gpu.shape, im_mask, self.im.shape)
+ if self.pil:
+ # convert im back to PIL and update draw
+ self.fromarray(self.im)
+
def rectangle(self, xy, fill=None, outline=None, width=1):
# Add rectangle to image (PIL-only)
self.draw.rectangle(xy, fill, outline, width)
@@ -124,6 +156,11 @@ def text(self, xy, text, txt_color=(255, 255, 255), anchor='top'):
xy[1] += 1 - h
self.draw.text(xy, text, fill=txt_color, font=self.font)
+ def fromarray(self, im):
+ # Update self.im from a numpy array
+ self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
+ self.draw = ImageDraw.Draw(self.im)
+
def result(self):
# Return annotated image as array
return np.asarray(self.im)
@@ -152,7 +189,6 @@ def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detec
ax[i].axis('off')
LOGGER.info(f'Saving {f}... ({n}/{channels})')
- plt.title('Features')
plt.savefig(f, dpi=300, bbox_inches='tight')
plt.close()
np.save(str(f.with_suffix('.npy')), x[0].cpu().numpy()) # npy save
@@ -180,26 +216,31 @@ def butter_lowpass(cutoff, fs, order):
return filtfilt(b, a, data) # forward-backward filter
-def output_to_target(output):
- # Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
+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
targets = []
for i, o in enumerate(output):
- targets.extend([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf] for *box, conf, cls in o.cpu().numpy())
- return np.array(targets)
+ box, conf, cls = o[:max_det, :6].cpu().split((4, 1, 1), 1)
+ j = torch.full((conf.shape[0], 1), i)
+ targets.append(torch.cat((j, cls, xyxy2xywh(box), conf), 1))
+ return torch.cat(targets, 0).numpy()
@threaded
-def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16):
+def plot_images(images, targets, paths=None, fname='images.jpg', names=None):
# Plot image grid with labels
if isinstance(images, torch.Tensor):
images = images.cpu().float().numpy()
if isinstance(targets, torch.Tensor):
targets = targets.cpu().numpy()
- if np.max(images[0]) <= 1:
- images *= 255 # de-normalise (optional)
+
+ max_size = 1920 # max image size
+ max_subplots = 16 # max image subplots, i.e. 4x4
bs, _, h, w = images.shape # batch size, _, height, width
bs = min(bs, max_subplots) # limit plot images
ns = np.ceil(bs ** 0.5) # number of subplots (square)
+ if np.max(images[0]) <= 1:
+ images *= 255 # de-normalise (optional)
# Build Image
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
@@ -409,7 +450,7 @@ def imshow_cls(im, labels=None, pred=None, names=None, nmax=25, verbose=False, f
plt.savefig(f, dpi=300, bbox_inches='tight')
plt.close()
if verbose:
- LOGGER.info(f"Saving {f}")
+ LOGGER.info(f'Saving {f}')
if labels is not None:
LOGGER.info('True: ' + ' '.join(f'{names[i]:3s}' for i in labels[:nmax]))
if pred is not None:
@@ -509,7 +550,7 @@ def save_one_box(xyxy, im, file=Path('im.jpg'), gain=1.02, pad=10, square=False,
b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square
b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad
xyxy = xywh2xyxy(b).long()
- clip_coords(xyxy, im.shape)
+ clip_boxes(xyxy, im.shape)
crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)]
if save:
file.parent.mkdir(parents=True, exist_ok=True) # make directory
diff --git a/utils/segment/__init__.py b/utils/segment/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/utils/segment/augmentations.py b/utils/segment/augmentations.py
new file mode 100644
index 000000000000..169addedf0f5
--- /dev/null
+++ b/utils/segment/augmentations.py
@@ -0,0 +1,104 @@
+# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
+"""
+Image augmentation functions
+"""
+
+import math
+import random
+
+import cv2
+import numpy as np
+
+from ..augmentations import box_candidates
+from ..general import resample_segments, segment2box
+
+
+def mixup(im, labels, segments, im2, labels2, segments2):
+ # Applies MixUp augmentation 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)
+ segments = np.concatenate((segments, segments2), 0)
+ return im, labels, segments
+
+
+def random_perspective(im,
+ targets=(),
+ segments=(),
+ degrees=10,
+ translate=.1,
+ scale=.1,
+ shear=10,
+ perspective=0.0,
+ border=(0, 0)):
+ # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10))
+ # targets = [cls, xyxy]
+
+ height = im.shape[0] + border[0] * 2 # shape(h,w,c)
+ width = im.shape[1] + border[1] * 2
+
+ # Center
+ C = np.eye(3)
+ C[0, 2] = -im.shape[1] / 2 # x translation (pixels)
+ C[1, 2] = -im.shape[0] / 2 # y translation (pixels)
+
+ # Perspective
+ P = np.eye(3)
+ P[2, 0] = random.uniform(-perspective, perspective) # x perspective (about y)
+ P[2, 1] = random.uniform(-perspective, perspective) # y perspective (about x)
+
+ # Rotation and Scale
+ R = np.eye(3)
+ a = random.uniform(-degrees, degrees)
+ # a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations
+ s = random.uniform(1 - scale, 1 + scale)
+ # s = 2 ** random.uniform(-scale, scale)
+ R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s)
+
+ # Shear
+ S = np.eye(3)
+ S[0, 1] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # x shear (deg)
+ S[1, 0] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # y shear (deg)
+
+ # Translation
+ T = np.eye(3)
+ T[0, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * width) # x translation (pixels)
+ T[1, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * height) # y translation (pixels)
+
+ # Combined rotation matrix
+ M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT
+ if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed
+ if perspective:
+ im = cv2.warpPerspective(im, M, dsize=(width, height), borderValue=(114, 114, 114))
+ else: # affine
+ im = cv2.warpAffine(im, M[:2], dsize=(width, height), borderValue=(114, 114, 114))
+
+ # Visualize
+ # import matplotlib.pyplot as plt
+ # ax = plt.subplots(1, 2, figsize=(12, 6))[1].ravel()
+ # ax[0].imshow(im[:, :, ::-1]) # base
+ # ax[1].imshow(im2[:, :, ::-1]) # warped
+
+ # Transform label coordinates
+ n = len(targets)
+ new_segments = []
+ if n:
+ new = np.zeros((n, 4))
+ segments = resample_segments(segments) # upsample
+ for i, segment in enumerate(segments):
+ xy = np.ones((len(segment), 3))
+ xy[:, :2] = segment
+ xy = xy @ M.T # transform
+ xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]) # perspective rescale or affine
+
+ # clip
+ new[i] = segment2box(xy, width, height)
+ new_segments.append(xy)
+
+ # filter candidates
+ i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01)
+ targets = targets[i]
+ targets[:, 1:5] = new[i]
+ new_segments = np.array(new_segments)[i]
+
+ return im, targets, new_segments
diff --git a/utils/segment/dataloaders.py b/utils/segment/dataloaders.py
new file mode 100644
index 000000000000..097a5d5cb058
--- /dev/null
+++ b/utils/segment/dataloaders.py
@@ -0,0 +1,332 @@
+# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
+"""
+Dataloaders
+"""
+
+import os
+import random
+
+import cv2
+import numpy as np
+import torch
+from torch.utils.data import DataLoader, distributed
+
+from ..augmentations import augment_hsv, copy_paste, letterbox
+from ..dataloaders import InfiniteDataLoader, LoadImagesAndLabels, seed_worker
+from ..general import LOGGER, xyn2xy, xywhn2xyxy, xyxy2xywhn
+from ..torch_utils import torch_distributed_zero_first
+from .augmentations import mixup, random_perspective
+
+RANK = int(os.getenv('RANK', -1))
+
+
+def create_dataloader(path,
+ imgsz,
+ batch_size,
+ stride,
+ single_cls=False,
+ hyp=None,
+ augment=False,
+ cache=False,
+ pad=0.0,
+ rect=False,
+ rank=-1,
+ workers=8,
+ image_weights=False,
+ quad=False,
+ prefix='',
+ shuffle=False,
+ mask_downsample_ratio=1,
+ overlap_mask=False,
+ seed=0):
+ if rect and shuffle:
+ LOGGER.warning('WARNING ⚠️ --rect is incompatible with DataLoader shuffle, setting shuffle=False')
+ shuffle = False
+ with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
+ dataset = LoadImagesAndLabelsAndMasks(
+ path,
+ imgsz,
+ batch_size,
+ augment=augment, # augmentation
+ hyp=hyp, # hyperparameters
+ rect=rect, # rectangular batches
+ cache_images=cache,
+ single_cls=single_cls,
+ stride=int(stride),
+ pad=pad,
+ image_weights=image_weights,
+ prefix=prefix,
+ downsample_ratio=mask_downsample_ratio,
+ overlap=overlap_mask)
+
+ batch_size = min(batch_size, len(dataset))
+ nd = torch.cuda.device_count() # number of CUDA devices
+ nw = min([os.cpu_count() // max(nd, 1), batch_size if batch_size > 1 else 0, workers]) # number of workers
+ sampler = None if rank == -1 else distributed.DistributedSampler(dataset, shuffle=shuffle)
+ loader = DataLoader if image_weights else InfiniteDataLoader # only DataLoader allows for attribute updates
+ generator = torch.Generator()
+ generator.manual_seed(6148914691236517205 + seed + RANK)
+ return loader(
+ dataset,
+ batch_size=batch_size,
+ shuffle=shuffle and sampler is None,
+ num_workers=nw,
+ sampler=sampler,
+ pin_memory=True,
+ collate_fn=LoadImagesAndLabelsAndMasks.collate_fn4 if quad else LoadImagesAndLabelsAndMasks.collate_fn,
+ worker_init_fn=seed_worker,
+ generator=generator,
+ ), dataset
+
+
+class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
+
+ def __init__(
+ self,
+ path,
+ img_size=640,
+ batch_size=16,
+ augment=False,
+ hyp=None,
+ rect=False,
+ image_weights=False,
+ cache_images=False,
+ single_cls=False,
+ stride=32,
+ pad=0,
+ min_items=0,
+ prefix='',
+ downsample_ratio=1,
+ overlap=False,
+ ):
+ super().__init__(path, img_size, batch_size, augment, hyp, rect, image_weights, cache_images, single_cls,
+ stride, pad, min_items, prefix)
+ self.downsample_ratio = downsample_ratio
+ self.overlap = overlap
+
+ def __getitem__(self, index):
+ index = self.indices[index] # linear, shuffled, or image_weights
+
+ hyp = self.hyp
+ mosaic = self.mosaic and random.random() < hyp['mosaic']
+ masks = []
+ if mosaic:
+ # Load mosaic
+ img, labels, segments = self.load_mosaic(index)
+ shapes = None
+
+ # MixUp augmentation
+ if random.random() < hyp['mixup']:
+ img, labels, segments = mixup(img, labels, segments, *self.load_mosaic(random.randint(0, self.n - 1)))
+
+ else:
+ # Load image
+ img, (h0, w0), (h, w) = self.load_image(index)
+
+ # Letterbox
+ shape = self.batch_shapes[self.batch[index]] if self.rect else self.img_size # final letterboxed shape
+ img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment)
+ shapes = (h0, w0), ((h / h0, w / w0), pad) # for COCO mAP rescaling
+
+ labels = self.labels[index].copy()
+ # [array, array, ....], array.shape=(num_points, 2), xyxyxyxy
+ segments = self.segments[index].copy()
+ if len(segments):
+ for i_s in range(len(segments)):
+ segments[i_s] = xyn2xy(
+ segments[i_s],
+ ratio[0] * w,
+ ratio[1] * h,
+ padw=pad[0],
+ padh=pad[1],
+ )
+ if labels.size: # normalized xywh to pixel xyxy format
+ labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1])
+
+ if self.augment:
+ img, labels, segments = random_perspective(img,
+ labels,
+ segments=segments,
+ degrees=hyp['degrees'],
+ translate=hyp['translate'],
+ scale=hyp['scale'],
+ shear=hyp['shear'],
+ perspective=hyp['perspective'])
+
+ nl = len(labels) # number of labels
+ if nl:
+ labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1e-3)
+ if self.overlap:
+ masks, sorted_idx = polygons2masks_overlap(img.shape[:2],
+ segments,
+ downsample_ratio=self.downsample_ratio)
+ masks = masks[None] # (640, 640) -> (1, 640, 640)
+ labels = labels[sorted_idx]
+ else:
+ masks = polygons2masks(img.shape[:2], segments, color=1, downsample_ratio=self.downsample_ratio)
+
+ masks = (torch.from_numpy(masks) if len(masks) else torch.zeros(1 if self.overlap else nl, img.shape[0] //
+ self.downsample_ratio, img.shape[1] //
+ self.downsample_ratio))
+ # TODO: albumentations support
+ if self.augment:
+ # Albumentations
+ # there are some augmentation that won't change boxes and masks,
+ # so just be it for now.
+ img, labels = self.albumentations(img, labels)
+ nl = len(labels) # update after albumentations
+
+ # HSV color-space
+ augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v'])
+
+ # Flip up-down
+ if random.random() < hyp['flipud']:
+ img = np.flipud(img)
+ if nl:
+ labels[:, 2] = 1 - labels[:, 2]
+ masks = torch.flip(masks, dims=[1])
+
+ # Flip left-right
+ if random.random() < hyp['fliplr']:
+ img = np.fliplr(img)
+ if nl:
+ labels[:, 1] = 1 - labels[:, 1]
+ masks = torch.flip(masks, dims=[2])
+
+ # Cutouts # labels = cutout(img, labels, p=0.5)
+
+ labels_out = torch.zeros((nl, 6))
+ if nl:
+ labels_out[:, 1:] = torch.from_numpy(labels)
+
+ # Convert
+ img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
+ img = np.ascontiguousarray(img)
+
+ 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
+ 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
+
+ # 3 additional image indices
+ indices = [index] + random.choices(self.indices, k=3) # 3 additional image indices
+ for i, index in enumerate(indices):
+ # Load image
+ img, _, (h, w) = self.load_image(index)
+
+ # place img in img4
+ if i == 0: # top left
+ img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
+ x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)
+ x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h # xmin, ymin, xmax, ymax (small image)
+ elif i == 1: # top right
+ x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc
+ x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h
+ elif i == 2: # bottom left
+ x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)
+ x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, w, min(y2a - y1a, h)
+ elif i == 3: # bottom right
+ x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)
+ x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)
+
+ img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] # img4[ymin:ymax, xmin:xmax]
+ padw = x1a - x1b
+ padh = y1a - y1b
+
+ labels, segments = self.labels[index].copy(), self.segments[index].copy()
+
+ if labels.size:
+ labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padw, padh) # normalized xywh to pixel xyxy format
+ segments = [xyn2xy(x, w, h, padw, padh) for x in segments]
+ labels4.append(labels)
+ segments4.extend(segments)
+
+ # Concat/clip labels
+ labels4 = np.concatenate(labels4, 0)
+ for x in (labels4[:, 1:], *segments4):
+ np.clip(x, 0, 2 * s, out=x) # clip when using random_perspective()
+ # img4, labels4 = replicate(img4, labels4) # replicate
+
+ # Augment
+ img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste'])
+ img4, labels4, segments4 = random_perspective(img4,
+ labels4,
+ segments4,
+ degrees=self.hyp['degrees'],
+ translate=self.hyp['translate'],
+ scale=self.hyp['scale'],
+ shear=self.hyp['shear'],
+ perspective=self.hyp['perspective'],
+ border=self.mosaic_border) # border to remove
+ return img4, labels4, segments4
+
+ @staticmethod
+ def collate_fn(batch):
+ img, label, path, shapes, masks = zip(*batch) # transposed
+ batched_masks = torch.cat(masks, 0)
+ for i, l in enumerate(label):
+ l[:, 0] = i # add target image index for build_targets()
+ return torch.stack(img, 0), torch.cat(label, 0), path, shapes, batched_masks
+
+
+def polygon2mask(img_size, polygons, color=1, downsample_ratio=1):
+ """
+ Args:
+ img_size (tuple): The image size.
+ polygons (np.ndarray): [N, M], N is the number of polygons,
+ M is the number of points(Be divided by 2).
+ """
+ mask = np.zeros(img_size, dtype=np.uint8)
+ polygons = np.asarray(polygons)
+ polygons = polygons.astype(np.int32)
+ shape = polygons.shape
+ polygons = polygons.reshape(shape[0], -1, 2)
+ cv2.fillPoly(mask, polygons, color=color)
+ nh, nw = (img_size[0] // downsample_ratio, img_size[1] // downsample_ratio)
+ # NOTE: fillPoly firstly then resize is trying the keep the same way
+ # of loss calculation when mask-ratio=1.
+ mask = cv2.resize(mask, (nw, nh))
+ return mask
+
+
+def polygons2masks(img_size, polygons, color, downsample_ratio=1):
+ """
+ Args:
+ img_size (tuple): The image size.
+ polygons (list[np.ndarray]): each polygon is [N, M],
+ N is the number of polygons,
+ M is the number of points(Be divided by 2).
+ """
+ masks = []
+ for si in range(len(polygons)):
+ mask = polygon2mask(img_size, [polygons[si].reshape(-1)], color, downsample_ratio)
+ masks.append(mask)
+ return np.array(masks)
+
+
+def polygons2masks_overlap(img_size, segments, downsample_ratio=1):
+ """Return a (640, 640) overlap mask."""
+ masks = np.zeros((img_size[0] // downsample_ratio, img_size[1] // downsample_ratio),
+ dtype=np.int32 if len(segments) > 255 else np.uint8)
+ areas = []
+ ms = []
+ for si in range(len(segments)):
+ mask = polygon2mask(
+ img_size,
+ [segments[si].reshape(-1)],
+ downsample_ratio=downsample_ratio,
+ color=1,
+ )
+ ms.append(mask)
+ areas.append(mask.sum())
+ areas = np.asarray(areas)
+ index = np.argsort(-areas)
+ ms = np.array(ms)[index]
+ for i in range(len(segments)):
+ mask = ms[i] * (i + 1)
+ masks = masks + mask
+ masks = np.clip(masks, a_min=0, a_max=i + 1)
+ return masks, index
diff --git a/utils/segment/general.py b/utils/segment/general.py
new file mode 100644
index 000000000000..9da894538665
--- /dev/null
+++ b/utils/segment/general.py
@@ -0,0 +1,160 @@
+import cv2
+import numpy as np
+import torch
+import torch.nn.functional as F
+
+
+def crop_mask(masks, boxes):
+ """
+ "Crop" predicted masks by zeroing out everything not in the predicted bbox.
+ Vectorized by Chong (thanks Chong).
+
+ Args:
+ - masks should be a size [h, w, n] tensor of masks
+ - boxes should be a size [n, 4] tensor of bbox coords in relative point form
+ """
+
+ n, h, w = masks.shape
+ x1, y1, x2, y2 = torch.chunk(boxes[:, :, None], 4, 1) # x1 shape(1,1,n)
+ r = torch.arange(w, device=masks.device, dtype=x1.dtype)[None, None, :] # rows shape(1,w,1)
+ c = torch.arange(h, device=masks.device, dtype=x1.dtype)[None, :, None] # cols shape(h,1,1)
+
+ return masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2))
+
+
+def process_mask_upsample(protos, masks_in, bboxes, shape):
+ """
+ Crop after upsample.
+ protos: [mask_dim, mask_h, mask_w]
+ masks_in: [n, mask_dim], n is number of masks after nms
+ bboxes: [n, 4], n is number of masks after nms
+ shape: input_image_size, (h, w)
+
+ return: h, w, n
+ """
+
+ c, mh, mw = protos.shape # CHW
+ masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw)
+ masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW
+ masks = crop_mask(masks, bboxes) # CHW
+ return masks.gt_(0.5)
+
+
+def process_mask(protos, masks_in, bboxes, shape, upsample=False):
+ """
+ Crop before upsample.
+ proto_out: [mask_dim, mask_h, mask_w]
+ out_masks: [n, mask_dim], n is number of masks after nms
+ bboxes: [n, 4], n is number of masks after nms
+ shape:input_image_size, (h, w)
+
+ return: h, w, n
+ """
+
+ c, mh, mw = protos.shape # CHW
+ ih, iw = shape
+ masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw) # CHW
+
+ downsampled_bboxes = bboxes.clone()
+ downsampled_bboxes[:, 0] *= mw / iw
+ downsampled_bboxes[:, 2] *= mw / iw
+ downsampled_bboxes[:, 3] *= mh / ih
+ downsampled_bboxes[:, 1] *= mh / ih
+
+ masks = crop_mask(masks, downsampled_bboxes) # CHW
+ if upsample:
+ masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW
+ return masks.gt_(0.5)
+
+
+def process_mask_native(protos, masks_in, bboxes, shape):
+ """
+ Crop after upsample.
+ protos: [mask_dim, mask_h, mask_w]
+ masks_in: [n, mask_dim], n is number of masks after nms
+ bboxes: [n, 4], n is number of masks after nms
+ shape: input_image_size, (h, w)
+
+ return: h, w, n
+ """
+ c, mh, mw = protos.shape # CHW
+ masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw)
+ gain = min(mh / shape[0], mw / shape[1]) # gain = old / new
+ pad = (mw - shape[1] * gain) / 2, (mh - shape[0] * gain) / 2 # wh padding
+ top, left = int(pad[1]), int(pad[0]) # y, x
+ bottom, right = int(mh - pad[1]), int(mw - pad[0])
+ masks = masks[:, top:bottom, left:right]
+
+ masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW
+ masks = crop_mask(masks, bboxes) # CHW
+ return masks.gt_(0.5)
+
+
+def scale_image(im1_shape, masks, im0_shape, ratio_pad=None):
+ """
+ img1_shape: model input shape, [h, w]
+ img0_shape: origin pic shape, [h, w, 3]
+ masks: [h, w, num]
+ """
+ # Rescale coordinates (xyxy) from im1_shape to im0_shape
+ if ratio_pad is None: # calculate from im0_shape
+ gain = min(im1_shape[0] / im0_shape[0], im1_shape[1] / im0_shape[1]) # gain = old / new
+ pad = (im1_shape[1] - im0_shape[1] * gain) / 2, (im1_shape[0] - im0_shape[0] * gain) / 2 # wh padding
+ else:
+ pad = ratio_pad[1]
+ top, left = int(pad[1]), int(pad[0]) # y, x
+ bottom, right = int(im1_shape[0] - pad[1]), int(im1_shape[1] - pad[0])
+
+ if len(masks.shape) < 2:
+ raise ValueError(f'"len of masks shape" should be 2 or 3, but got {len(masks.shape)}')
+ masks = masks[top:bottom, left:right]
+ # masks = masks.permute(2, 0, 1).contiguous()
+ # masks = F.interpolate(masks[None], im0_shape[:2], mode='bilinear', align_corners=False)[0]
+ # masks = masks.permute(1, 2, 0).contiguous()
+ masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]))
+
+ if len(masks.shape) == 2:
+ masks = masks[:, :, None]
+ return masks
+
+
+def mask_iou(mask1, mask2, eps=1e-7):
+ """
+ mask1: [N, n] m1 means number of predicted objects
+ mask2: [M, n] m2 means number of gt objects
+ Note: n means image_w x image_h
+
+ return: masks iou, [N, M]
+ """
+ intersection = torch.matmul(mask1, mask2.t()).clamp(0)
+ union = (mask1.sum(1)[:, None] + mask2.sum(1)[None]) - intersection # (area1 + area2) - intersection
+ return intersection / (union + eps)
+
+
+def masks_iou(mask1, mask2, eps=1e-7):
+ """
+ mask1: [N, n] m1 means number of predicted objects
+ mask2: [N, n] m2 means number of gt objects
+ Note: n means image_w x image_h
+
+ return: masks iou, (N, )
+ """
+ intersection = (mask1 * mask2).sum(1).clamp(0) # (N, )
+ union = (mask1.sum(1) + mask2.sum(1))[None] - intersection # (area1 + area2) - intersection
+ return intersection / (union + eps)
+
+
+def masks2segments(masks, strategy='largest'):
+ # Convert masks(n,160,160) into segments(n,xy)
+ segments = []
+ for x in masks.int().cpu().numpy().astype('uint8'):
+ c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
+ if c:
+ if strategy == 'concat': # concatenate all segments
+ c = np.concatenate([x.reshape(-1, 2) for x in c])
+ elif strategy == 'largest': # select largest segment
+ c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2)
+ else:
+ c = np.zeros((0, 2)) # no segments found
+ segments.append(c.astype('float32'))
+ return segments
diff --git a/utils/segment/loss.py b/utils/segment/loss.py
new file mode 100644
index 000000000000..2a8a4c680f6f
--- /dev/null
+++ b/utils/segment/loss.py
@@ -0,0 +1,186 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from ..general import xywh2xyxy
+from ..loss import FocalLoss, smooth_BCE
+from ..metrics import bbox_iou
+from ..torch_utils import de_parallel
+from .general import crop_mask
+
+
+class ComputeLoss:
+ # Compute losses
+ def __init__(self, model, autobalance=False, overlap=False):
+ self.sort_obj_iou = False
+ self.overlap = overlap
+ device = next(model.parameters()).device # get model device
+ h = model.hyp # hyperparameters
+ self.device = device
+
+ # Define criteria
+ BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
+ BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
+
+ # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
+ self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets
+
+ # Focal loss
+ g = h['fl_gamma'] # focal loss gamma
+ if g > 0:
+ BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
+
+ m = de_parallel(model).model[-1] # Detect() module
+ self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) # P3-P7
+ self.ssi = list(m.stride).index(16) if autobalance else 0 # stride 16 index
+ self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
+ self.na = m.na # number of anchors
+ self.nc = m.nc # number of classes
+ self.nl = m.nl # number of layers
+ self.nm = m.nm # number of masks
+ self.anchors = m.anchors
+ self.device = device
+
+ def __call__(self, preds, targets, masks): # predictions, targets, model
+ p, proto = preds
+ bs, nm, mask_h, mask_w = proto.shape # batch size, number of masks, mask height, mask width
+ lcls = torch.zeros(1, device=self.device)
+ lbox = torch.zeros(1, device=self.device)
+ lobj = torch.zeros(1, device=self.device)
+ lseg = torch.zeros(1, device=self.device)
+ tcls, tbox, indices, anchors, tidxs, xywhn = self.build_targets(p, targets) # targets
+
+ # Losses
+ for i, pi in enumerate(p): # layer index, layer predictions
+ b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
+ tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device) # target obj
+
+ n = b.shape[0] # number of targets
+ if n:
+ pxy, pwh, _, pcls, pmask = pi[b, a, gj, gi].split((2, 2, 1, self.nc, nm), 1) # subset of predictions
+
+ # Box regression
+ pxy = pxy.sigmoid() * 2 - 0.5
+ pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
+ pbox = torch.cat((pxy, pwh), 1) # predicted box
+ iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze() # iou(prediction, target)
+ lbox += (1.0 - iou).mean() # iou loss
+
+ # Objectness
+ iou = iou.detach().clamp(0).type(tobj.dtype)
+ if self.sort_obj_iou:
+ j = iou.argsort()
+ b, a, gj, gi, iou = b[j], a[j], gj[j], gi[j], iou[j]
+ if self.gr < 1:
+ iou = (1.0 - self.gr) + self.gr * iou
+ tobj[b, a, gj, gi] = iou # iou ratio
+
+ # Classification
+ if self.nc > 1: # cls loss (only if multiple classes)
+ t = torch.full_like(pcls, self.cn, device=self.device) # targets
+ t[range(n), tcls[i]] = self.cp
+ lcls += self.BCEcls(pcls, t) # BCE
+
+ # Mask regression
+ if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample
+ masks = F.interpolate(masks[None], (mask_h, mask_w), mode='nearest')[0]
+ marea = xywhn[i][:, 2:].prod(1) # mask width, height normalized
+ mxyxy = xywh2xyxy(xywhn[i] * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device))
+ for bi in b.unique():
+ j = b == bi # matching index
+ if self.overlap:
+ mask_gti = torch.where(masks[bi][None] == tidxs[i][j].view(-1, 1, 1), 1.0, 0.0)
+ else:
+ mask_gti = masks[tidxs[i]][j]
+ lseg += self.single_mask_loss(mask_gti, pmask[j], proto[bi], mxyxy[j], marea[j])
+
+ obji = self.BCEobj(pi[..., 4], tobj)
+ lobj += obji * self.balance[i] # obj loss
+ if self.autobalance:
+ self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()
+
+ if self.autobalance:
+ self.balance = [x / self.balance[self.ssi] for x in self.balance]
+ lbox *= self.hyp['box']
+ lobj *= self.hyp['obj']
+ lcls *= self.hyp['cls']
+ lseg *= self.hyp['box'] / bs
+
+ loss = lbox + lobj + lcls + lseg
+ 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
+ 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)
+ 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
+ ai = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
+ if self.overlap:
+ batch = p[0].shape[0]
+ ti = []
+ for i in range(batch):
+ num = (targets[:, 0] == i).sum() # find number of targets of each image
+ ti.append(torch.arange(num, device=self.device).float().view(1, num).repeat(na, 1) + 1) # (na, num)
+ ti = torch.cat(ti, 1) # (na, nt)
+ else:
+ ti = torch.arange(nt, device=self.device).float().view(1, nt).repeat(na, 1)
+ targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None], ti[..., None]), 2) # append anchor indices
+
+ g = 0.5 # bias
+ off = torch.tensor(
+ [
+ [0, 0],
+ [1, 0],
+ [0, 1],
+ [-1, 0],
+ [0, -1], # j,k,l,m
+ # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
+ ],
+ device=self.device).float() * g # offsets
+
+ for i in range(self.nl):
+ anchors, shape = self.anchors[i], p[i].shape
+ gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]] # xyxy gain
+
+ # Match targets to anchors
+ t = targets * gain # shape(3,n,7)
+ if nt:
+ # Matches
+ r = t[..., 4:6] / anchors[:, None] # wh ratio
+ j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare
+ # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
+ t = t[j] # filter
+
+ # Offsets
+ gxy = t[:, 2:4] # grid xy
+ gxi = gain[[2, 3]] - gxy # inverse
+ j, k = ((gxy % 1 < g) & (gxy > 1)).T
+ l, m = ((gxi % 1 < g) & (gxi > 1)).T
+ j = torch.stack((torch.ones_like(j), j, k, l, m))
+ t = t.repeat((5, 1, 1))[j]
+ offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
+ else:
+ t = targets[0]
+ offsets = 0
+
+ # Define
+ bc, gxy, gwh, at = t.chunk(4, 1) # (image, class), grid xy, grid wh, anchors
+ (a, tidx), (b, c) = at.long().T, bc.long().T # anchors, image, class
+ gij = (gxy - offsets).long()
+ gi, gj = gij.T # grid indices
+
+ # Append
+ indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1))) # image, anchor, grid
+ tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
+ anch.append(anchors[a]) # anchors
+ tcls.append(c) # class
+ tidxs.append(tidx)
+ xywhn.append(torch.cat((gxy, gwh), 1) / gain[2:6]) # xywh normalized
+
+ return tcls, tbox, indices, anch, tidxs, xywhn
diff --git a/utils/segment/metrics.py b/utils/segment/metrics.py
new file mode 100644
index 000000000000..c9f137e38ead
--- /dev/null
+++ b/utils/segment/metrics.py
@@ -0,0 +1,210 @@
+# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
+"""
+Model validation metrics
+"""
+
+import numpy as np
+
+from ..metrics import ap_per_class
+
+
+def fitness(x):
+ # Model fitness as a weighted combination of metrics
+ w = [0.0, 0.0, 0.1, 0.9, 0.0, 0.0, 0.1, 0.9]
+ return (x[:, :8] * w).sum(1)
+
+
+def ap_per_class_box_and_mask(
+ tp_m,
+ tp_b,
+ conf,
+ pred_cls,
+ target_cls,
+ plot=False,
+ save_dir='.',
+ names=(),
+):
+ """
+ Args:
+ tp_b: tp of boxes.
+ tp_m: tp of masks.
+ other arguments see `func: ap_per_class`.
+ """
+ results_boxes = ap_per_class(tp_b,
+ conf,
+ pred_cls,
+ target_cls,
+ plot=plot,
+ save_dir=save_dir,
+ names=names,
+ prefix='Box')[2:]
+ results_masks = ap_per_class(tp_m,
+ conf,
+ pred_cls,
+ target_cls,
+ plot=plot,
+ save_dir=save_dir,
+ names=names,
+ prefix='Mask')[2:]
+
+ results = {
+ 'boxes': {
+ 'p': results_boxes[0],
+ 'r': results_boxes[1],
+ 'ap': results_boxes[3],
+ 'f1': results_boxes[2],
+ 'ap_class': results_boxes[4]},
+ 'masks': {
+ 'p': results_masks[0],
+ 'r': results_masks[1],
+ 'ap': results_masks[3],
+ 'f1': results_masks[2],
+ 'ap_class': results_masks[4]}}
+ return results
+
+
+class Metric:
+
+ def __init__(self) -> None:
+ self.p = [] # (nc, )
+ self.r = [] # (nc, )
+ self.f1 = [] # (nc, )
+ self.all_ap = [] # (nc, 10)
+ self.ap_class_index = [] # (nc, )
+
+ @property
+ def ap50(self):
+ """AP@0.5 of all classes.
+ Return:
+ (nc, ) or [].
+ """
+ return self.all_ap[:, 0] if len(self.all_ap) else []
+
+ @property
+ def ap(self):
+ """AP@0.5:0.95
+ Return:
+ (nc, ) or [].
+ """
+ return self.all_ap.mean(1) if len(self.all_ap) else []
+
+ @property
+ def mp(self):
+ """mean precision of all classes.
+ Return:
+ float.
+ """
+ return self.p.mean() if len(self.p) else 0.0
+
+ @property
+ def mr(self):
+ """mean recall of all classes.
+ Return:
+ float.
+ """
+ return self.r.mean() if len(self.r) else 0.0
+
+ @property
+ def map50(self):
+ """Mean AP@0.5 of all classes.
+ Return:
+ float.
+ """
+ return self.all_ap[:, 0].mean() if len(self.all_ap) else 0.0
+
+ @property
+ def map(self):
+ """Mean AP@0.5:0.95 of all classes.
+ Return:
+ float.
+ """
+ return self.all_ap.mean() if len(self.all_ap) else 0.0
+
+ def mean_results(self):
+ """Mean of results, return mp, mr, map50, map"""
+ return (self.mp, self.mr, self.map50, self.map)
+
+ def class_result(self, i):
+ """class-aware result, return p[i], r[i], ap50[i], ap[i]"""
+ return (self.p[i], self.r[i], self.ap50[i], self.ap[i])
+
+ def get_maps(self, nc):
+ maps = np.zeros(nc) + self.map
+ for i, c in enumerate(self.ap_class_index):
+ maps[c] = self.ap[i]
+ return maps
+
+ def update(self, results):
+ """
+ Args:
+ results: tuple(p, r, ap, f1, ap_class)
+ """
+ p, r, all_ap, f1, ap_class_index = results
+ self.p = p
+ self.r = r
+ self.all_ap = all_ap
+ self.f1 = f1
+ self.ap_class_index = ap_class_index
+
+
+class Metrics:
+ """Metric for boxes and masks."""
+
+ def __init__(self) -> None:
+ self.metric_box = Metric()
+ self.metric_mask = Metric()
+
+ def update(self, results):
+ """
+ Args:
+ results: Dict{'boxes': Dict{}, 'masks': Dict{}}
+ """
+ self.metric_box.update(list(results['boxes'].values()))
+ self.metric_mask.update(list(results['masks'].values()))
+
+ def mean_results(self):
+ return self.metric_box.mean_results() + self.metric_mask.mean_results()
+
+ def class_result(self, i):
+ return self.metric_box.class_result(i) + self.metric_mask.class_result(i)
+
+ def get_maps(self, nc):
+ 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
+ return self.metric_box.ap_class_index
+
+
+KEYS = [
+ 'train/box_loss',
+ 'train/seg_loss', # train loss
+ 'train/obj_loss',
+ 'train/cls_loss',
+ 'metrics/precision(B)',
+ 'metrics/recall(B)',
+ 'metrics/mAP_0.5(B)',
+ 'metrics/mAP_0.5:0.95(B)', # metrics
+ 'metrics/precision(M)',
+ 'metrics/recall(M)',
+ 'metrics/mAP_0.5(M)',
+ 'metrics/mAP_0.5:0.95(M)', # metrics
+ 'val/box_loss',
+ 'val/seg_loss', # val loss
+ 'val/obj_loss',
+ 'val/cls_loss',
+ 'x/lr0',
+ 'x/lr1',
+ 'x/lr2',]
+
+BEST_KEYS = [
+ 'best/epoch',
+ 'best/precision(B)',
+ 'best/recall(B)',
+ 'best/mAP_0.5(B)',
+ 'best/mAP_0.5:0.95(B)',
+ 'best/precision(M)',
+ 'best/recall(M)',
+ 'best/mAP_0.5(M)',
+ 'best/mAP_0.5:0.95(M)',]
diff --git a/utils/segment/plots.py b/utils/segment/plots.py
new file mode 100644
index 000000000000..1b22ec838ac9
--- /dev/null
+++ b/utils/segment/plots.py
@@ -0,0 +1,143 @@
+import contextlib
+import math
+from pathlib import Path
+
+import cv2
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+import torch
+
+from .. import threaded
+from ..general import xywh2xyxy
+from ..plots import Annotator, colors
+
+
+@threaded
+def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg', names=None):
+ # Plot image grid with labels
+ if isinstance(images, torch.Tensor):
+ images = images.cpu().float().numpy()
+ if isinstance(targets, torch.Tensor):
+ targets = targets.cpu().numpy()
+ if isinstance(masks, torch.Tensor):
+ masks = masks.cpu().numpy().astype(int)
+
+ max_size = 1920 # max image size
+ max_subplots = 16 # max image subplots, i.e. 4x4
+ bs, _, h, w = images.shape # batch size, _, height, width
+ bs = min(bs, max_subplots) # limit plot images
+ ns = np.ceil(bs ** 0.5) # number of subplots (square)
+ if np.max(images[0]) <= 1:
+ images *= 255 # de-normalise (optional)
+
+ # Build Image
+ mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
+ for i, im in enumerate(images):
+ if i == max_subplots: # if last batch has fewer images than we expect
+ break
+ x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin
+ im = im.transpose(1, 2, 0)
+ mosaic[y:y + h, x:x + w, :] = im
+
+ # Resize (optional)
+ scale = max_size / ns / max(h, w)
+ if scale < 1:
+ h = math.ceil(scale * h)
+ w = math.ceil(scale * w)
+ mosaic = cv2.resize(mosaic, tuple(int(x * ns) for x in (w, h)))
+
+ # Annotate
+ fs = int((h + w) * ns * 0.01) # font size
+ annotator = Annotator(mosaic, line_width=round(fs / 10), font_size=fs, pil=True, example=names)
+ for i in range(i + 1):
+ x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin
+ annotator.rectangle([x, y, x + w, y + h], None, (255, 255, 255), width=2) # borders
+ if paths:
+ annotator.text((x + 5, y + 5), text=Path(paths[i]).name[:40], txt_color=(220, 220, 220)) # filenames
+ if len(targets) > 0:
+ idx = targets[:, 0] == i
+ ti = targets[idx] # image targets
+
+ boxes = xywh2xyxy(ti[:, 2:6]).T
+ classes = ti[:, 1].astype('int')
+ labels = ti.shape[1] == 6 # labels if no conf column
+ conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred)
+
+ if boxes.shape[1]:
+ if boxes.max() <= 1.01: # if normalized with tolerance 0.01
+ boxes[[0, 2]] *= w # scale to pixels
+ boxes[[1, 3]] *= h
+ elif scale < 1: # absolute coords need scale if image scales
+ boxes *= scale
+ boxes[[0, 2]] += x
+ boxes[[1, 3]] += y
+ for j, box in enumerate(boxes.T.tolist()):
+ cls = classes[j]
+ color = colors(cls)
+ cls = names[cls] if names else cls
+ if labels or conf[j] > 0.25: # 0.25 conf thresh
+ label = f'{cls}' if labels else f'{cls} {conf[j]:.1f}'
+ annotator.box_label(box, label, color=color)
+
+ # Plot masks
+ if len(masks):
+ if masks.max() > 1.0: # mean that masks are overlap
+ image_masks = masks[[i]] # (1, 640, 640)
+ nl = len(ti)
+ index = np.arange(nl).reshape(nl, 1, 1) + 1
+ image_masks = np.repeat(image_masks, nl, axis=0)
+ image_masks = np.where(image_masks == index, 1.0, 0.0)
+ else:
+ image_masks = masks[idx]
+
+ im = np.asarray(annotator.im).copy()
+ for j, box in enumerate(boxes.T.tolist()):
+ if labels or conf[j] > 0.25: # 0.25 conf thresh
+ color = colors(classes[j])
+ mh, mw = image_masks[j].shape
+ if mh != h or mw != w:
+ mask = image_masks[j].astype(np.uint8)
+ mask = cv2.resize(mask, (w, h))
+ mask = mask.astype(bool)
+ else:
+ mask = image_masks[j].astype(bool)
+ with contextlib.suppress(Exception):
+ im[y:y + h, x:x + w, :][mask] = im[y:y + h, x:x + w, :][mask] * 0.4 + np.array(color) * 0.6
+ annotator.fromarray(im)
+ annotator.im.save(fname) # save
+
+
+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')
+ 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()
+ files = list(save_dir.glob('results*.csv'))
+ assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.'
+ for f in files:
+ try:
+ data = pd.read_csv(f)
+ index = np.argmax(0.9 * data.values[:, 8] + 0.1 * data.values[:, 7] + 0.9 * data.values[:, 12] +
+ 0.1 * data.values[:, 11])
+ s = [x.strip() for x in data.columns]
+ x = data.values[:, 0]
+ for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]):
+ y = data.values[:, j]
+ # y[y == 0] = np.nan # don't show zero values
+ ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=2)
+ if best:
+ # best
+ ax[i].scatter(index, y[index], color='r', label=f'best:{index}', marker='*', linewidth=3)
+ ax[i].set_title(s[j] + f'\n{round(y[index], 5)}')
+ else:
+ # last
+ ax[i].scatter(x[-1], y[-1], color='r', label='last', marker='*', linewidth=3)
+ ax[i].set_title(s[j] + f'\n{round(y[-1], 5)}')
+ # if j in [8, 9, 10]: # share train and val loss y axes
+ # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
+ except Exception as e:
+ print(f'Warning: Plotting error for {f}: {e}')
+ ax[1].legend()
+ fig.savefig(save_dir / 'results.png', dpi=200)
+ plt.close()
diff --git a/utils/torch_utils.py b/utils/torch_utils.py
index 8a3366ca3e27..5b67b3fa7a06 100644
--- a/utils/torch_utils.py
+++ b/utils/torch_utils.py
@@ -32,6 +32,7 @@
# Suppress PyTorch warnings
warnings.filterwarnings('ignore', message='User provided device_type of \'cuda\', but CUDA is not available. Disabling')
+warnings.filterwarnings('ignore', category=UserWarning)
def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
@@ -47,7 +48,7 @@ def smartCrossEntropyLoss(label_smoothing=0.0):
if check_version(torch.__version__, '1.10.0'):
return nn.CrossEntropyLoss(label_smoothing=label_smoothing)
if label_smoothing > 0:
- LOGGER.warning(f'WARNING: label smoothing {label_smoothing} requires torch>=1.10.0')
+ LOGGER.warning(f'WARNING ⚠️ label smoothing {label_smoothing} requires torch>=1.10.0')
return nn.CrossEntropyLoss()
@@ -81,7 +82,7 @@ def reshape_classifier_output(model, n=1000):
elif nn.Conv2d in types:
i = types.index(nn.Conv2d) # nn.Conv2d index
if m[i].out_channels != n:
- m[i] = nn.Conv2d(m[i].in_channels, n, m[i].kernel_size, m[i].stride, bias=m[i].bias)
+ m[i] = nn.Conv2d(m[i].in_channels, n, m[i].kernel_size, m[i].stride, bias=m[i].bias is not None)
@contextmanager
@@ -290,7 +291,7 @@ def model_info(model, verbose=False, imgsz=640):
fs = ''
name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model'
- LOGGER.info(f"{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
+ LOGGER.info(f'{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}')
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
@@ -319,12 +320,13 @@ def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):
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():
- if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter): # bias (no decay)
- g[2].append(v.bias)
- if isinstance(v, bn): # weight (no decay)
- g[1].append(v.weight)
- elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter): # weight (with decay)
- g[0].append(v.weight)
+ for p_name, p in v.named_parameters(recurse=0):
+ if p_name == 'bias': # bias (no decay)
+ g[2].append(p)
+ elif p_name == 'weight' and isinstance(v, bn): # weight (no decay)
+ g[1].append(p)
+ else:
+ g[0].append(p) # weight (with decay)
if name == 'Adam':
optimizer = torch.optim.Adam(g[2], lr=lr, betas=(momentum, 0.999)) # adjust beta1 to momentum
@@ -340,7 +342,7 @@ def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):
optimizer.add_param_group({'params': g[0], 'weight_decay': decay}) # add g0 with weight_decay
optimizer.add_param_group({'params': g[1], 'weight_decay': 0.0}) # add g1 (BatchNorm2d weights)
LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups "
- f"{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias")
+ f'{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias')
return optimizer
diff --git a/utils/triton.py b/utils/triton.py
new file mode 100644
index 000000000000..25928021477e
--- /dev/null
+++ b/utils/triton.py
@@ -0,0 +1,85 @@
+# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
+""" Utils to interact with the Triton Inference Server
+"""
+
+import typing
+from urllib.parse import urlparse
+
+import torch
+
+
+class TritonRemoteModel:
+ """ A wrapper over a model served by the Triton Inference Server. It can
+ be configured to communicate over GRPC or HTTP. It accepts Torch Tensors
+ as input and returns them as outputs.
+ """
+
+ def __init__(self, url: str):
+ """
+ Keyword arguments:
+ url: Fully qualified address of the Triton server - for e.g. grpc://localhost:8000
+ """
+
+ parsed_url = urlparse(url)
+ if parsed_url.scheme == 'grpc':
+ from tritonclient.grpc import InferenceServerClient, InferInput
+
+ self.client = InferenceServerClient(parsed_url.netloc) # Triton GRPC client
+ model_repository = self.client.get_model_repository_index()
+ self.model_name = model_repository.models[0].name
+ self.metadata = self.client.get_model_metadata(self.model_name, as_json=True)
+
+ def create_input_placeholders() -> typing.List[InferInput]:
+ return [
+ InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']]
+
+ else:
+ from tritonclient.http import InferenceServerClient, InferInput
+
+ self.client = InferenceServerClient(parsed_url.netloc) # Triton HTTP client
+ model_repository = self.client.get_model_repository_index()
+ self.model_name = model_repository[0]['name']
+ self.metadata = self.client.get_model_metadata(self.model_name)
+
+ def create_input_placeholders() -> typing.List[InferInput]:
+ return [
+ InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']]
+
+ self._create_input_placeholders_fn = create_input_placeholders
+
+ @property
+ def runtime(self):
+ """Returns the model runtime"""
+ return self.metadata.get('backend', self.metadata.get('platform'))
+
+ def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[torch.Tensor, ...]]:
+ """ Invokes the model. Parameters can be provided via args or kwargs.
+ args, if provided, are assumed to match the order of inputs of the model.
+ kwargs are matched with the model input names.
+ """
+ inputs = self._create_inputs(*args, **kwargs)
+ response = self.client.infer(model_name=self.model_name, inputs=inputs)
+ result = []
+ for output in self.metadata['outputs']:
+ tensor = torch.as_tensor(response.as_numpy(output['name']))
+ result.append(tensor)
+ return result[0] if len(result) == 1 else result
+
+ def _create_inputs(self, *args, **kwargs):
+ args_len, kwargs_len = len(args), len(kwargs)
+ if not args_len and not kwargs_len:
+ raise RuntimeError('No inputs provided.')
+ if args_len and kwargs_len:
+ raise RuntimeError('Cannot specify args and kwargs at the same time')
+
+ placeholders = self._create_input_placeholders_fn()
+ if args_len:
+ if args_len != len(placeholders):
+ raise RuntimeError(f'Expected {len(placeholders)} inputs, got {args_len}.')
+ for input, value in zip(placeholders, args):
+ input.set_data_from_numpy(value.cpu().numpy())
+ else:
+ for input in placeholders:
+ value = kwargs[input.name]
+ input.set_data_from_numpy(value.cpu().numpy())
+ return placeholders
diff --git a/val.py b/val.py
index 4b0bdddae3b1..d4073b42fe78 100644
--- a/val.py
+++ b/val.py
@@ -9,7 +9,7 @@
$ python val.py --weights yolov5s.pt # PyTorch
yolov5s.torchscript # TorchScript
yolov5s.onnx # ONNX Runtime or OpenCV DNN with --dnn
- yolov5s.xml # OpenVINO
+ yolov5s_openvino_model # OpenVINO
yolov5s.engine # TensorRT
yolov5s.mlmodel # CoreML (macOS-only)
yolov5s_saved_model # TensorFlow SavedModel
@@ -22,6 +22,7 @@
import argparse
import json
import os
+import subprocess
import sys
from pathlib import Path
@@ -38,9 +39,9 @@
from models.common import DetectMultiBackend
from utils.callbacks import Callbacks
from utils.dataloaders import create_dataloader
-from utils.general import (LOGGER, Profile, check_dataset, check_img_size, check_requirements, check_yaml,
- coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
- scale_coords, xywh2xyxy, xyxy2xywh)
+from utils.general import (LOGGER, TQDM_BAR_FORMAT, Profile, check_dataset, check_img_size, check_requirements,
+ check_yaml, coco80_to_coco91_class, colorstr, increment_path, non_max_suppression,
+ print_args, scale_boxes, xywh2xyxy, xyxy2xywh)
from utils.metrics import ConfusionMatrix, ap_per_class, box_iou
from utils.plots import output_to_target, plot_images, plot_val_study
from utils.torch_utils import select_device, smart_inference_mode
@@ -71,12 +72,12 @@ def save_one_json(predn, jdict, path, class_map):
def process_batch(detections, labels, iouv):
"""
- Return correct predictions matrix. Both sets of boxes are in (x1, y1, x2, y2) format.
+ Return correct prediction matrix
Arguments:
- detections (Array[N, 6]), x1, y1, x2, y2, conf, class
- labels (Array[M, 5]), class, x1, y1, x2, y2
+ detections (array[N, 6]), x1, y1, x2, y2, conf, class
+ labels (array[M, 5]), class, x1, y1, x2, y2
Returns:
- correct (Array[N, 10]), for 10 IoU levels
+ correct (array[N, 10]), for 10 IoU levels
"""
correct = np.zeros((detections.shape[0], iouv.shape[0])).astype(bool)
iou = box_iou(labels[:, 1:], detections[:, :4])
@@ -102,6 +103,7 @@ def run(
imgsz=640, # inference size (pixels)
conf_thres=0.001, # confidence threshold
iou_thres=0.6, # NMS IoU threshold
+ max_det=300, # maximum detections per image
task='val', # train, val, test, speed or study
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
workers=8, # max dataloader workers (per RANK in DDP mode)
@@ -168,8 +170,7 @@ def run(
assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \
f'classes). Pass correct combination of --weights and --data that are trained together.'
model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup
- pad = 0.0 if task in ('speed', 'benchmark') else 0.5
- rect = False if task == 'benchmark' else pt # square inference for benchmarks
+ pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
dataloader = create_dataloader(data[task],
imgsz,
@@ -187,12 +188,13 @@ def run(
if isinstance(names, (list, tuple)): # old format
names = dict(enumerate(names))
class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
- s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
- dt, p, r, f1, mp, mr, map50, map = (Profile(), Profile(), Profile()), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP50', 'mAP50-95')
+ tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ dt = Profile(), Profile(), Profile() # profiling times
loss = torch.zeros(3, device=device)
jdict, stats, ap, ap_class = [], [], [], []
callbacks.run('on_val_start')
- pbar = tqdm(dataloader, desc=s, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
+ pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
callbacks.run('on_val_batch_start')
with dt[0]:
@@ -205,7 +207,7 @@ def run(
# Inference
with dt[1]:
- out, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
+ preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
# Loss
if compute_loss:
@@ -215,10 +217,16 @@ def run(
targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
with dt[2]:
- out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
+ preds = non_max_suppression(preds,
+ conf_thres,
+ iou_thres,
+ labels=lb,
+ multi_label=True,
+ agnostic=single_cls,
+ max_det=max_det)
# Metrics
- for si, pred in enumerate(out):
+ for si, pred in enumerate(preds):
labels = targets[targets[:, 0] == si, 1:]
nl, npr = labels.shape[0], pred.shape[0] # number of labels, predictions
path, shape = Path(paths[si]), shapes[si][0]
@@ -236,12 +244,12 @@ def run(
if single_cls:
pred[:, 5] = 0
predn = pred.clone()
- scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
+ scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
# Evaluate
if nl:
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
- scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
+ scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
correct = process_batch(predn, labelsn, iouv)
if plots:
@@ -258,9 +266,9 @@ def run(
# Plot images
if plots and batch_i < 3:
plot_images(im, targets, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names) # labels
- plot_images(im, output_to_target(out), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred
+ plot_images(im, output_to_target(preds), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred
- callbacks.run('on_val_batch_end', batch_i, im, targets, paths, shapes, out)
+ callbacks.run('on_val_batch_end', batch_i, im, targets, paths, shapes, preds)
# Compute metrics
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
@@ -274,7 +282,7 @@ def run(
pf = '%22s' + '%11i' * 2 + '%11.3g' * 4 # print format
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
if nt.sum() == 0:
- LOGGER.warning(f'WARNING: no labels found in {task} set, can not compute metrics without labels ⚠️')
+ LOGGER.warning(f'WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels')
# Print results per class
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
@@ -295,14 +303,14 @@ def run(
# Save JSON
if save_json and len(jdict):
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
- anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
- pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
+ anno_json = str(Path('../datasets/coco/annotations/instances_val2017.json')) # annotations
+ pred_json = str(save_dir / f'{w}_predictions.json') # predictions
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
with open(pred_json, 'w') as f:
json.dump(jdict, f)
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
- check_requirements('pycocotools')
+ check_requirements('pycocotools>=2.0.6')
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
@@ -332,11 +340,12 @@ def run(
def parse_opt():
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.pt path(s)')
+ parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)')
parser.add_argument('--batch-size', type=int, default=32, help='batch size')
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
+ parser.add_argument('--max-det', type=int, default=300, help='maximum detections per image')
parser.add_argument('--task', default='val', help='train, val, test, speed or study')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
@@ -365,14 +374,14 @@ def main(opt):
if opt.task in ('train', 'val', 'test'): # run normally
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
- LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} > 0.001 produces invalid results ⚠️')
+ LOGGER.info(f'WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results')
if opt.save_hybrid:
- LOGGER.info('WARNING: --save-hybrid will return high mAP from hybrid labels, not from predictions alone ⚠️')
+ LOGGER.info('WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone')
run(**vars(opt))
else:
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
- opt.half = True # FP16 for fastest results
+ opt.half = torch.cuda.is_available() and opt.device != 'cpu' # FP16 for fastest results
if opt.task == 'speed': # speed benchmarks
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
@@ -389,10 +398,12 @@ def main(opt):
r, _, t = run(**vars(opt), plots=False)
y.append(r + t) # results and times
np.savetxt(f, y, fmt='%10.4g') # save
- os.system('zip -r study.zip study_*.txt')
+ subprocess.run(['zip', '-r', 'study.zip', 'study_*.txt'])
plot_val_study(x=x) # plot
+ else:
+ raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")')
-if __name__ == "__main__":
+if __name__ == '__main__':
opt = parse_opt()
main(opt)