ultralytics 8.3.110__py3-none-any.whl → 8.3.112__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ultralytics/__init__.py +1 -1
- ultralytics/cfg/__init__.py +14 -16
- ultralytics/cfg/datasets/coco8-multispectral.yaml +104 -0
- ultralytics/data/augment.py +19 -6
- ultralytics/data/base.py +24 -26
- ultralytics/data/converter.py +52 -3
- ultralytics/data/dataset.py +5 -5
- ultralytics/data/loaders.py +7 -9
- ultralytics/data/split.py +123 -0
- ultralytics/data/utils.py +34 -52
- ultralytics/engine/exporter.py +22 -24
- ultralytics/engine/model.py +3 -6
- ultralytics/engine/predictor.py +5 -3
- ultralytics/engine/results.py +7 -7
- ultralytics/engine/trainer.py +4 -5
- ultralytics/engine/tuner.py +1 -1
- ultralytics/engine/validator.py +4 -4
- ultralytics/hub/auth.py +1 -1
- ultralytics/hub/session.py +3 -3
- ultralytics/models/rtdetr/train.py +1 -22
- ultralytics/models/sam/modules/sam.py +2 -1
- ultralytics/models/yolo/classify/train.py +1 -1
- ultralytics/models/yolo/detect/train.py +2 -2
- ultralytics/models/yolo/detect/val.py +1 -1
- ultralytics/models/yolo/obb/train.py +1 -1
- ultralytics/models/yolo/pose/predict.py +1 -1
- ultralytics/models/yolo/pose/train.py +4 -2
- ultralytics/models/yolo/pose/val.py +1 -1
- ultralytics/models/yolo/segment/train.py +1 -1
- ultralytics/models/yolo/segment/val.py +1 -1
- ultralytics/models/yolo/world/train.py +1 -1
- ultralytics/models/yolo/world/train_world.py +1 -0
- ultralytics/models/yolo/yoloe/train.py +2 -2
- ultralytics/models/yolo/yoloe/train_seg.py +2 -2
- ultralytics/nn/autobackend.py +20 -18
- ultralytics/nn/modules/block.py +1 -1
- ultralytics/nn/modules/head.py +4 -0
- ultralytics/nn/tasks.py +13 -11
- ultralytics/solutions/instance_segmentation.py +1 -1
- ultralytics/solutions/object_blurrer.py +1 -1
- ultralytics/solutions/object_cropper.py +2 -2
- ultralytics/solutions/parking_management.py +1 -1
- ultralytics/solutions/security_alarm.py +1 -1
- ultralytics/solutions/solutions.py +3 -6
- ultralytics/trackers/byte_tracker.py +1 -1
- ultralytics/trackers/utils/gmc.py +4 -4
- ultralytics/utils/__init__.py +29 -22
- ultralytics/utils/autobatch.py +4 -4
- ultralytics/utils/benchmarks.py +8 -8
- ultralytics/utils/callbacks/clearml.py +1 -1
- ultralytics/utils/callbacks/comet.py +5 -5
- ultralytics/utils/callbacks/dvc.py +1 -1
- ultralytics/utils/callbacks/mlflow.py +2 -1
- ultralytics/utils/callbacks/neptune.py +1 -1
- ultralytics/utils/callbacks/tensorboard.py +7 -9
- ultralytics/utils/checks.py +20 -26
- ultralytics/utils/downloads.py +4 -4
- ultralytics/utils/export.py +1 -1
- ultralytics/utils/metrics.py +1 -1
- ultralytics/utils/ops.py +1 -1
- ultralytics/utils/patches.py +8 -1
- ultralytics/utils/plotting.py +27 -29
- ultralytics/utils/tal.py +1 -1
- ultralytics/utils/torch_utils.py +4 -4
- ultralytics/utils/tuner.py +2 -2
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/METADATA +1 -1
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/RECORD +71 -69
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/WHEEL +1 -1
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/entry_points.txt +0 -0
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/licenses/LICENSE +0 -0
- {ultralytics-8.3.110.dist-info → ultralytics-8.3.112.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
import random
|
2
|
+
import shutil
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from ultralytics.data.utils import IMG_FORMATS, img2label_paths
|
6
|
+
from ultralytics.utils import DATASETS_DIR, LOGGER, TQDM
|
7
|
+
|
8
|
+
|
9
|
+
def split_classify_dataset(source_dir, train_ratio=0.8):
|
10
|
+
"""
|
11
|
+
Split dataset into train and val directories in a new directory.
|
12
|
+
|
13
|
+
Creates a new directory '{source_dir}_split' with train/val subdirectories, preserving the original class
|
14
|
+
structure with an 80/20 split by default.
|
15
|
+
|
16
|
+
Directory structure:
|
17
|
+
Before:
|
18
|
+
caltech/
|
19
|
+
├── class1/
|
20
|
+
│ ├── img1.jpg
|
21
|
+
│ ├── img2.jpg
|
22
|
+
│ └── ...
|
23
|
+
├── class2/
|
24
|
+
│ ├── img1.jpg
|
25
|
+
│ └── ...
|
26
|
+
└── ...
|
27
|
+
|
28
|
+
After:
|
29
|
+
caltech_split/
|
30
|
+
├── train/
|
31
|
+
│ ├── class1/
|
32
|
+
│ │ ├── img1.jpg
|
33
|
+
│ │ └── ...
|
34
|
+
│ ├── class2/
|
35
|
+
│ │ ├── img1.jpg
|
36
|
+
│ │ └── ...
|
37
|
+
│ └── ...
|
38
|
+
└── val/
|
39
|
+
├── class1/
|
40
|
+
│ ├── img2.jpg
|
41
|
+
│ └── ...
|
42
|
+
├── class2/
|
43
|
+
│ └── ...
|
44
|
+
└── ...
|
45
|
+
|
46
|
+
Args:
|
47
|
+
source_dir (str | Path): Path to Caltech dataset root directory.
|
48
|
+
train_ratio (float): Ratio for train split, between 0 and 1.
|
49
|
+
|
50
|
+
Examples:
|
51
|
+
>>> # Split dataset with default 80/20 ratio
|
52
|
+
>>> split_classify_dataset("path/to/caltech")
|
53
|
+
>>> # Split with custom ratio
|
54
|
+
>>> split_classify_dataset("path/to/caltech", 0.75)
|
55
|
+
"""
|
56
|
+
source_path = Path(source_dir)
|
57
|
+
split_path = Path(f"{source_path}_split")
|
58
|
+
train_path, val_path = split_path / "train", split_path / "val"
|
59
|
+
|
60
|
+
# Create directory structure
|
61
|
+
split_path.mkdir(exist_ok=True)
|
62
|
+
train_path.mkdir(exist_ok=True)
|
63
|
+
val_path.mkdir(exist_ok=True)
|
64
|
+
|
65
|
+
# Process class directories
|
66
|
+
class_dirs = [d for d in source_path.iterdir() if d.is_dir()]
|
67
|
+
total_images = sum(len(list(d.glob("*.*"))) for d in class_dirs)
|
68
|
+
stats = f"{len(class_dirs)} classes, {total_images} images"
|
69
|
+
LOGGER.info(f"Splitting {source_path} ({stats}) into {train_ratio:.0%} train, {1 - train_ratio:.0%} val...")
|
70
|
+
|
71
|
+
for class_dir in class_dirs:
|
72
|
+
# Create class directories
|
73
|
+
(train_path / class_dir.name).mkdir(exist_ok=True)
|
74
|
+
(val_path / class_dir.name).mkdir(exist_ok=True)
|
75
|
+
|
76
|
+
# Split and copy files
|
77
|
+
image_files = list(class_dir.glob("*.*"))
|
78
|
+
random.shuffle(image_files)
|
79
|
+
split_idx = int(len(image_files) * train_ratio)
|
80
|
+
|
81
|
+
for img in image_files[:split_idx]:
|
82
|
+
shutil.copy2(img, train_path / class_dir.name / img.name)
|
83
|
+
|
84
|
+
for img in image_files[split_idx:]:
|
85
|
+
shutil.copy2(img, val_path / class_dir.name / img.name)
|
86
|
+
|
87
|
+
LOGGER.info(f"Split complete in {split_path} ✅")
|
88
|
+
return split_path
|
89
|
+
|
90
|
+
|
91
|
+
def autosplit(path=DATASETS_DIR / "coco8/images", weights=(0.9, 0.1, 0.0), annotated_only=False):
|
92
|
+
"""
|
93
|
+
Automatically split a dataset into train/val/test splits and save the resulting splits into autosplit_*.txt files.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
path (Path, optional): Path to images directory.
|
97
|
+
weights (list | tuple, optional): Train, validation, and test split fractions.
|
98
|
+
annotated_only (bool, optional): If True, only images with an associated txt file are used.
|
99
|
+
|
100
|
+
Examples:
|
101
|
+
>>> from ultralytics.data.split import autosplit
|
102
|
+
>>> autosplit()
|
103
|
+
"""
|
104
|
+
path = Path(path) # images dir
|
105
|
+
files = sorted(x for x in path.rglob("*.*") if x.suffix[1:].lower() in IMG_FORMATS) # image files only
|
106
|
+
n = len(files) # number of files
|
107
|
+
random.seed(0) # for reproducibility
|
108
|
+
indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split
|
109
|
+
|
110
|
+
txt = ["autosplit_train.txt", "autosplit_val.txt", "autosplit_test.txt"] # 3 txt files
|
111
|
+
for x in txt:
|
112
|
+
if (path.parent / x).exists():
|
113
|
+
(path.parent / x).unlink() # remove existing
|
114
|
+
|
115
|
+
LOGGER.info(f"Autosplitting images from {path}" + ", using *.txt labeled images only" * annotated_only)
|
116
|
+
for i, img in TQDM(zip(indices, files), total=n):
|
117
|
+
if not annotated_only or Path(img2label_paths([str(img)])[0]).exists(): # check label
|
118
|
+
with open(path.parent / txt[i], "a", encoding="utf-8") as f:
|
119
|
+
f.write(f"./{img.relative_to(path.parent).as_posix()}" + "\n") # add image to txt file
|
120
|
+
|
121
|
+
|
122
|
+
if __name__ == "__main__":
|
123
|
+
split_classify_dataset("../datasets/caltech101")
|
ultralytics/data/utils.py
CHANGED
@@ -66,7 +66,7 @@ def check_file_speeds(files, threshold_ms=10, max_files=5, prefix=""):
|
|
66
66
|
>>> check_file_speeds(image_files, threshold_ms=15)
|
67
67
|
"""
|
68
68
|
if not files or len(files) == 0:
|
69
|
-
LOGGER.warning(f"{prefix}
|
69
|
+
LOGGER.warning(f"{prefix}Image speed checks: No files to check")
|
70
70
|
return
|
71
71
|
|
72
72
|
# Sample files (max 5)
|
@@ -96,7 +96,7 @@ def check_file_speeds(files, threshold_ms=10, max_files=5, prefix=""):
|
|
96
96
|
pass
|
97
97
|
|
98
98
|
if not ping_times:
|
99
|
-
LOGGER.warning(f"{prefix}
|
99
|
+
LOGGER.warning(f"{prefix}Image speed checks: failed to access files")
|
100
100
|
return
|
101
101
|
|
102
102
|
# Calculate stats with uncertainties
|
@@ -116,7 +116,7 @@ def check_file_speeds(files, threshold_ms=10, max_files=5, prefix=""):
|
|
116
116
|
LOGGER.info(f"{prefix}Fast image access ✅ ({ping_msg}{speed_msg}{size_msg})")
|
117
117
|
else:
|
118
118
|
LOGGER.warning(
|
119
|
-
f"{prefix}
|
119
|
+
f"{prefix}Slow image access detected ({ping_msg}{speed_msg}{size_msg}). "
|
120
120
|
f"Use local storage instead of remote/mounted storage for better performance. "
|
121
121
|
f"See https://docs.ultralytics.com/guides/model-training-tips/"
|
122
122
|
)
|
@@ -166,11 +166,11 @@ def verify_image(args):
|
|
166
166
|
f.seek(-2, 2)
|
167
167
|
if f.read() != b"\xff\xd9": # corrupt JPEG
|
168
168
|
ImageOps.exif_transpose(Image.open(im_file)).save(im_file, "JPEG", subsampling=0, quality=100)
|
169
|
-
msg = f"{prefix}
|
169
|
+
msg = f"{prefix}{im_file}: corrupt JPEG restored and saved"
|
170
170
|
nf = 1
|
171
171
|
except Exception as e:
|
172
172
|
nc = 1
|
173
|
-
msg = f"{prefix}
|
173
|
+
msg = f"{prefix}{im_file}: ignoring corrupt image/label: {e}"
|
174
174
|
return (im_file, cls), nf, nc, msg
|
175
175
|
|
176
176
|
|
@@ -192,7 +192,7 @@ def verify_image_label(args):
|
|
192
192
|
f.seek(-2, 2)
|
193
193
|
if f.read() != b"\xff\xd9": # corrupt JPEG
|
194
194
|
ImageOps.exif_transpose(Image.open(im_file)).save(im_file, "JPEG", subsampling=0, quality=100)
|
195
|
-
msg = f"{prefix}
|
195
|
+
msg = f"{prefix}{im_file}: corrupt JPEG restored and saved"
|
196
196
|
|
197
197
|
# Verify labels
|
198
198
|
if os.path.isfile(lb_file):
|
@@ -227,7 +227,7 @@ def verify_image_label(args):
|
|
227
227
|
lb = lb[i] # remove duplicates
|
228
228
|
if segments:
|
229
229
|
segments = [segments[x] for x in i]
|
230
|
-
msg = f"{prefix}
|
230
|
+
msg = f"{prefix}{im_file}: {nl - len(i)} duplicate labels removed"
|
231
231
|
else:
|
232
232
|
ne = 1 # label empty
|
233
233
|
lb = np.zeros((0, (5 + nkpt * ndim) if keypoint else 5), dtype=np.float32)
|
@@ -243,7 +243,7 @@ def verify_image_label(args):
|
|
243
243
|
return im_file, lb, shape, segments, keypoints, nm, nf, ne, nc, msg
|
244
244
|
except Exception as e:
|
245
245
|
nc = 1
|
246
|
-
msg = f"{prefix}
|
246
|
+
msg = f"{prefix}{im_file}: ignoring corrupt image/label: {e}"
|
247
247
|
return [None, None, None, None, None, nm, nf, ne, nc, msg]
|
248
248
|
|
249
249
|
|
@@ -408,7 +408,7 @@ def check_det_dataset(dataset, autodownload=True):
|
|
408
408
|
raise SyntaxError(
|
409
409
|
emojis(f"{dataset} '{k}:' key missing ❌.\n'train' and 'val' are required in all data YAMLs.")
|
410
410
|
)
|
411
|
-
LOGGER.
|
411
|
+
LOGGER.warning("renaming data YAML 'validation' key to 'val' to match YOLO format.")
|
412
412
|
data["val"] = data.pop("validation") # replace 'validation' key with 'val' key
|
413
413
|
if "names" not in data and "nc" not in data:
|
414
414
|
raise SyntaxError(emojis(f"{dataset} key missing ❌.\n either 'names' or 'nc' are required in all data YAMLs."))
|
@@ -420,6 +420,7 @@ def check_det_dataset(dataset, autodownload=True):
|
|
420
420
|
data["nc"] = len(data["names"])
|
421
421
|
|
422
422
|
data["names"] = check_class_names(data["names"])
|
423
|
+
data["channels"] = data.get("channels", 3) # get image channels, default to 3
|
423
424
|
|
424
425
|
# Resolve paths
|
425
426
|
path = Path(extract_dir or data.get("path") or Path(data.get("yaml_file", "")).parent) # dataset root
|
@@ -444,7 +445,8 @@ def check_det_dataset(dataset, autodownload=True):
|
|
444
445
|
val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path
|
445
446
|
if not all(x.exists() for x in val):
|
446
447
|
name = clean_url(dataset) # dataset name with URL auth stripped
|
447
|
-
|
448
|
+
LOGGER.info("")
|
449
|
+
m = f"Dataset '{name}' images not found, missing path '{[x for x in val if not x.exists()][0]}'"
|
448
450
|
if s and autodownload:
|
449
451
|
LOGGER.warning(m)
|
450
452
|
else:
|
@@ -496,16 +498,27 @@ def check_cls_dataset(dataset, split=""):
|
|
496
498
|
dataset = Path(dataset)
|
497
499
|
data_dir = (dataset if dataset.is_dir() else (DATASETS_DIR / dataset)).resolve()
|
498
500
|
if not data_dir.is_dir():
|
499
|
-
LOGGER.
|
501
|
+
LOGGER.info("")
|
502
|
+
LOGGER.warning(f"Dataset not found, missing path {data_dir}, attempting download...")
|
500
503
|
t = time.time()
|
501
504
|
if str(dataset) == "imagenet":
|
502
505
|
subprocess.run(f"bash {ROOT / 'data/scripts/get_imagenet.sh'}", shell=True, check=True)
|
503
506
|
else:
|
504
507
|
url = f"https://github.com/ultralytics/assets/releases/download/v0.0.0/{dataset}.zip"
|
505
508
|
download(url, dir=data_dir.parent)
|
506
|
-
|
507
|
-
LOGGER.info(s)
|
509
|
+
LOGGER.info(f"Dataset download success ✅ ({time.time() - t:.1f}s), saved to {colorstr('bold', data_dir)}\n")
|
508
510
|
train_set = data_dir / "train"
|
511
|
+
if not train_set.is_dir():
|
512
|
+
LOGGER.warning(f"Dataset 'split=train' not found at {train_set}")
|
513
|
+
image_files = list(data_dir.rglob("*.jpg")) + list(data_dir.rglob("*.png"))
|
514
|
+
if image_files:
|
515
|
+
from ultralytics.data.split import split_classify_dataset
|
516
|
+
|
517
|
+
LOGGER.info(f"Found {len(image_files)} images in subdirectories. Attempting to split...")
|
518
|
+
data_dir = split_classify_dataset(data_dir, train_ratio=0.8)
|
519
|
+
train_set = data_dir / "train"
|
520
|
+
else:
|
521
|
+
LOGGER.error(f"No images found in {data_dir} or its subdirectories.")
|
509
522
|
val_set = (
|
510
523
|
data_dir / "val"
|
511
524
|
if (data_dir / "val").exists()
|
@@ -515,10 +528,10 @@ def check_cls_dataset(dataset, split=""):
|
|
515
528
|
) # data/test or data/val
|
516
529
|
test_set = data_dir / "test" if (data_dir / "test").exists() else None # data/val or data/test
|
517
530
|
if split == "val" and not val_set:
|
518
|
-
LOGGER.warning("
|
531
|
+
LOGGER.warning("Dataset 'split=val' not found, using 'split=test' instead.")
|
519
532
|
val_set = test_set
|
520
533
|
elif split == "test" and not test_set:
|
521
|
-
LOGGER.warning("
|
534
|
+
LOGGER.warning("Dataset 'split=test' not found, using 'split=val' instead.")
|
522
535
|
test_set = val_set
|
523
536
|
|
524
537
|
nc = len([x for x in (data_dir / "train").glob("*") if x.is_dir()]) # number of classes
|
@@ -536,15 +549,15 @@ def check_cls_dataset(dataset, split=""):
|
|
536
549
|
nd = len({file.parent for file in files}) # number of directories
|
537
550
|
if nf == 0:
|
538
551
|
if k == "train":
|
539
|
-
raise FileNotFoundError(
|
552
|
+
raise FileNotFoundError(f"{dataset} '{k}:' no training images found")
|
540
553
|
else:
|
541
|
-
LOGGER.warning(f"{prefix} found {nf} images in {nd} classes
|
554
|
+
LOGGER.warning(f"{prefix} found {nf} images in {nd} classes (no images found)")
|
542
555
|
elif nd != nc:
|
543
|
-
LOGGER.
|
556
|
+
LOGGER.error(f"{prefix} found {nf} images in {nd} classes (requires {nc} classes, not {nd})")
|
544
557
|
else:
|
545
558
|
LOGGER.info(f"{prefix} found {nf} images in {nd} classes ✅ ")
|
546
559
|
|
547
|
-
return {"train": train_set, "val": val_set, "test": test_set, "nc": nc, "names": names}
|
560
|
+
return {"train": train_set, "val": val_set, "test": test_set, "nc": nc, "names": names, "channels": 3}
|
548
561
|
|
549
562
|
|
550
563
|
class HUBDatasetStats:
|
@@ -728,7 +741,7 @@ def compress_one_image(f, f_new=None, max_dim=1920, quality=50):
|
|
728
741
|
im = im.resize((int(im.width * r), int(im.height * r)))
|
729
742
|
im.save(f_new or f, "JPEG", quality=quality, optimize=True) # save
|
730
743
|
except Exception as e: # use OpenCV
|
731
|
-
LOGGER.
|
744
|
+
LOGGER.warning(f"HUB ops PIL failure {f}: {e}")
|
732
745
|
im = cv2.imread(f)
|
733
746
|
im_height, im_width = im.shape[:2]
|
734
747
|
r = max_dim / max(im_height, im_width) # ratio
|
@@ -737,37 +750,6 @@ def compress_one_image(f, f_new=None, max_dim=1920, quality=50):
|
|
737
750
|
cv2.imwrite(str(f_new or f), im)
|
738
751
|
|
739
752
|
|
740
|
-
def autosplit(path=DATASETS_DIR / "coco8/images", weights=(0.9, 0.1, 0.0), annotated_only=False):
|
741
|
-
"""
|
742
|
-
Automatically split a dataset into train/val/test splits and save the resulting splits into autosplit_*.txt files.
|
743
|
-
|
744
|
-
Args:
|
745
|
-
path (Path, optional): Path to images directory.
|
746
|
-
weights (list | tuple, optional): Train, validation, and test split fractions.
|
747
|
-
annotated_only (bool, optional): If True, only images with an associated txt file are used.
|
748
|
-
|
749
|
-
Examples:
|
750
|
-
>>> from ultralytics.data.utils import autosplit
|
751
|
-
>>> autosplit()
|
752
|
-
"""
|
753
|
-
path = Path(path) # images dir
|
754
|
-
files = sorted(x for x in path.rglob("*.*") if x.suffix[1:].lower() in IMG_FORMATS) # image files only
|
755
|
-
n = len(files) # number of files
|
756
|
-
random.seed(0) # for reproducibility
|
757
|
-
indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split
|
758
|
-
|
759
|
-
txt = ["autosplit_train.txt", "autosplit_val.txt", "autosplit_test.txt"] # 3 txt files
|
760
|
-
for x in txt:
|
761
|
-
if (path.parent / x).exists():
|
762
|
-
(path.parent / x).unlink() # remove existing
|
763
|
-
|
764
|
-
LOGGER.info(f"Autosplitting images from {path}" + ", using *.txt labeled images only" * annotated_only)
|
765
|
-
for i, img in TQDM(zip(indices, files), total=n):
|
766
|
-
if not annotated_only or Path(img2label_paths([str(img)])[0]).exists(): # check label
|
767
|
-
with open(path.parent / txt[i], "a", encoding="utf-8") as f:
|
768
|
-
f.write(f"./{img.relative_to(path.parent).as_posix()}" + "\n") # add image to txt file
|
769
|
-
|
770
|
-
|
771
753
|
def load_dataset_cache_file(path):
|
772
754
|
"""Load an Ultralytics *.cache dictionary from path."""
|
773
755
|
import gc
|
@@ -788,4 +770,4 @@ def save_dataset_cache_file(prefix, path, x, version):
|
|
788
770
|
np.save(file, x)
|
789
771
|
LOGGER.info(f"{prefix}New cache created: {path}")
|
790
772
|
else:
|
791
|
-
LOGGER.warning(f"{prefix}
|
773
|
+
LOGGER.warning(f"{prefix}Cache directory {path.parent} is not writeable, cache not saved.")
|
ultralytics/engine/exporter.py
CHANGED
@@ -192,7 +192,7 @@ def try_export(inner_func):
|
|
192
192
|
LOGGER.info(f"{prefix} export success ✅ {dt.t:.1f}s, saved as '{f}' ({file_size(f):.1f} MB)")
|
193
193
|
return f, model
|
194
194
|
except Exception as e:
|
195
|
-
LOGGER.error(f"{prefix} export failure
|
195
|
+
LOGGER.error(f"{prefix} export failure {dt.t:.1f}s: {e}")
|
196
196
|
raise e
|
197
197
|
|
198
198
|
return outer_func
|
@@ -262,7 +262,7 @@ class Exporter:
|
|
262
262
|
matches = difflib.get_close_matches(fmt, fmts, n=1, cutoff=0.6) # 60% similarity required to match
|
263
263
|
if not matches:
|
264
264
|
raise ValueError(f"Invalid export format='{fmt}'. Valid formats are {fmts}")
|
265
|
-
LOGGER.warning(f"
|
265
|
+
LOGGER.warning(f"Invalid export format='{fmt}', updating to format='{matches[0]}'")
|
266
266
|
fmt = matches[0]
|
267
267
|
flags = [x == fmt for x in fmts]
|
268
268
|
if sum(flags) != 1:
|
@@ -276,16 +276,14 @@ class Exporter:
|
|
276
276
|
# Device
|
277
277
|
dla = None
|
278
278
|
if fmt == "engine" and self.args.device is None:
|
279
|
-
LOGGER.warning("
|
279
|
+
LOGGER.warning("TensorRT requires GPU export, automatically assigning device=0")
|
280
280
|
self.args.device = "0"
|
281
281
|
if fmt == "engine" and "dla" in str(self.args.device): # convert int/list to str first
|
282
282
|
dla = self.args.device.split(":")[-1]
|
283
283
|
self.args.device = "0" # update device to "0"
|
284
284
|
assert dla in {"0", "1"}, f"Expected self.args.device='dla:0' or 'dla:1, but got {self.args.device}."
|
285
285
|
if imx and self.args.device is None and torch.cuda.is_available():
|
286
|
-
LOGGER.warning(
|
287
|
-
"WARNING ⚠️ Exporting on CPU while CUDA is available, setting device=0 for faster export on GPU."
|
288
|
-
)
|
286
|
+
LOGGER.warning("Exporting on CPU while CUDA is available, setting device=0 for faster export on GPU.")
|
289
287
|
self.args.device = "0" # update device to "0"
|
290
288
|
self.device = select_device("cpu" if self.args.device is None else self.args.device)
|
291
289
|
|
@@ -294,7 +292,7 @@ class Exporter:
|
|
294
292
|
validate_args(fmt, self.args, fmt_keys)
|
295
293
|
if imx:
|
296
294
|
if not self.args.int8:
|
297
|
-
LOGGER.warning("
|
295
|
+
LOGGER.warning("IMX export requires int8=True, setting int8=True.")
|
298
296
|
self.args.int8 = True
|
299
297
|
if model.task != "detect":
|
300
298
|
raise ValueError("IMX export only supported for detection models.")
|
@@ -302,10 +300,10 @@ class Exporter:
|
|
302
300
|
model.names = default_class_names()
|
303
301
|
model.names = check_class_names(model.names)
|
304
302
|
if self.args.half and self.args.int8:
|
305
|
-
LOGGER.warning("
|
303
|
+
LOGGER.warning("half=True and int8=True are mutually exclusive, setting half=False.")
|
306
304
|
self.args.half = False
|
307
305
|
if self.args.half and onnx and self.device.type == "cpu":
|
308
|
-
LOGGER.warning("
|
306
|
+
LOGGER.warning("half=True only compatible with GPU export, i.e. use device=0")
|
309
307
|
self.args.half = False
|
310
308
|
self.imgsz = check_imgsz(self.args.imgsz, stride=model.stride, min_dim=2) # check image size
|
311
309
|
if self.args.int8 and engine:
|
@@ -316,7 +314,7 @@ class Exporter:
|
|
316
314
|
if rknn:
|
317
315
|
if not self.args.name:
|
318
316
|
LOGGER.warning(
|
319
|
-
"
|
317
|
+
"Rockchip RKNN export requires a missing 'name' arg for processor type. "
|
320
318
|
"Using default name='rk3588'."
|
321
319
|
)
|
322
320
|
self.args.name = "rk3588"
|
@@ -330,7 +328,7 @@ class Exporter:
|
|
330
328
|
assert not isinstance(model, ClassificationModel), "'nms=True' is not valid for classification models."
|
331
329
|
assert not (tflite and ARM64 and LINUX), "TFLite export with NMS unsupported on ARM64 Linux"
|
332
330
|
if getattr(model, "end2end", False):
|
333
|
-
LOGGER.warning("
|
331
|
+
LOGGER.warning("'nms=True' is not available for end2end models. Forcing 'nms=False'.")
|
334
332
|
self.args.nms = False
|
335
333
|
self.args.conf = self.args.conf or 0.25 # set conf default value for nms export
|
336
334
|
if edgetpu:
|
@@ -339,12 +337,12 @@ class Exporter:
|
|
339
337
|
"Edge TPU export only supported on non-aarch64 Linux. See https://coral.ai/docs/edgetpu/compiler"
|
340
338
|
)
|
341
339
|
elif self.args.batch != 1: # see github.com/ultralytics/ultralytics/pull/13420
|
342
|
-
LOGGER.warning("
|
340
|
+
LOGGER.warning("Edge TPU export requires batch size 1, setting batch=1.")
|
343
341
|
self.args.batch = 1
|
344
342
|
if isinstance(model, WorldModel):
|
345
343
|
LOGGER.warning(
|
346
|
-
"
|
347
|
-
"
|
344
|
+
"YOLOWorld (original version) export is not supported to any format. "
|
345
|
+
"YOLOWorldv2 models (i.e. 'yolov8s-worldv2.pt') only support export to "
|
348
346
|
"(torchscript, onnx, openvino, engine, coreml) formats. "
|
349
347
|
"See https://docs.ultralytics.com/models/yolo-world for details."
|
350
348
|
)
|
@@ -353,14 +351,13 @@ class Exporter:
|
|
353
351
|
if self.args.int8 and not self.args.data:
|
354
352
|
self.args.data = DEFAULT_CFG.data or TASK2DATA[getattr(model, "task", "detect")] # assign default data
|
355
353
|
LOGGER.warning(
|
356
|
-
"
|
357
|
-
f"Using default 'data={self.args.data}'."
|
354
|
+
"INT8 export requires a missing 'data' arg for calibration. Using default 'data={self.args.data}'."
|
358
355
|
)
|
359
356
|
if tfjs and (ARM64 and LINUX):
|
360
357
|
raise SystemError("TF.js exports are not currently supported on ARM64 Linux")
|
361
358
|
|
362
359
|
# Input
|
363
|
-
im = torch.zeros(self.args.batch, 3, *self.imgsz).to(self.device)
|
360
|
+
im = torch.zeros(self.args.batch, model.yaml.get("channels", 3), *self.imgsz).to(self.device)
|
364
361
|
file = Path(
|
365
362
|
getattr(model, "pt_path", None) or getattr(model, "yaml_file", None) or model.yaml.get("yaml_file", "")
|
366
363
|
)
|
@@ -436,6 +433,7 @@ class Exporter:
|
|
436
433
|
"imgsz": self.imgsz,
|
437
434
|
"names": model.names,
|
438
435
|
"args": {k: v for k, v in self.args if k in fmt_keys},
|
436
|
+
"channels": model.yaml.get("channels", 3),
|
439
437
|
} # model metadata
|
440
438
|
if dla is not None:
|
441
439
|
self.metadata["dla"] = dla # make sure `AutoBackend` uses correct dla device if it has one
|
@@ -528,7 +526,7 @@ class Exporter:
|
|
528
526
|
f"('batch={self.args.batch}')."
|
529
527
|
)
|
530
528
|
elif n < 300:
|
531
|
-
LOGGER.warning(f"{prefix}
|
529
|
+
LOGGER.warning(f"{prefix} >300 images recommended for INT8 calibration, found {n} images.")
|
532
530
|
return build_dataloader(dataset, batch=batch, workers=0) # required for batch loading
|
533
531
|
|
534
532
|
@try_export
|
@@ -741,7 +739,7 @@ class Exporter:
|
|
741
739
|
pnnx = name if name.is_file() else (ROOT / name)
|
742
740
|
if not pnnx.is_file():
|
743
741
|
LOGGER.warning(
|
744
|
-
f"{prefix}
|
742
|
+
f"{prefix} PNNX not found. Attempting to download binary file from "
|
745
743
|
"https://github.com/pnnx/pnnx/.\nNote PNNX Binary file must be placed in current working directory "
|
746
744
|
f"or in {ROOT}. See PNNX repo for full installation instructions."
|
747
745
|
)
|
@@ -754,7 +752,7 @@ class Exporter:
|
|
754
752
|
except Exception as e:
|
755
753
|
release = "20240410"
|
756
754
|
asset = f"pnnx-{release}-{system}.zip"
|
757
|
-
LOGGER.warning(f"{prefix}
|
755
|
+
LOGGER.warning(f"{prefix} PNNX GitHub assets not found: {e}, using default {asset}")
|
758
756
|
unzip_dir = safe_download(f"https://github.com/pnnx/pnnx/releases/download/{release}/{asset}", delete=True)
|
759
757
|
if check_is_path_safe(Path.cwd(), unzip_dir): # avoid path traversal security vulnerability
|
760
758
|
shutil.move(src=unzip_dir / name, dst=pnnx) # move binary to ROOT
|
@@ -819,7 +817,7 @@ class Exporter:
|
|
819
817
|
model = IOSDetectModel(self.model, self.im) if self.args.nms else self.model
|
820
818
|
else:
|
821
819
|
if self.args.nms:
|
822
|
-
LOGGER.warning(f"{prefix}
|
820
|
+
LOGGER.warning(f"{prefix} 'nms=True' is only available for Detect models like 'yolo11n.pt'.")
|
823
821
|
# TODO CoreML Segment and Pose model pipelining
|
824
822
|
model = self.model
|
825
823
|
ts = torch.jit.trace(model.eval(), self.im, strict=False) # TorchScript model
|
@@ -864,7 +862,7 @@ class Exporter:
|
|
864
862
|
ct_model.save(str(f)) # save *.mlpackage
|
865
863
|
except Exception as e:
|
866
864
|
LOGGER.warning(
|
867
|
-
f"{prefix}
|
865
|
+
f"{prefix} CoreML export to *.mlpackage failed ({e}), reverting to *.mlmodel export. "
|
868
866
|
f"Known coremltools Python 3.11 and Windows bugs https://github.com/apple/coremltools/issues/1928."
|
869
867
|
)
|
870
868
|
f = f.with_suffix(".mlmodel")
|
@@ -1033,7 +1031,7 @@ class Exporter:
|
|
1033
1031
|
@try_export
|
1034
1032
|
def export_edgetpu(self, tflite_model="", prefix=colorstr("Edge TPU:")):
|
1035
1033
|
"""YOLO Edge TPU export https://coral.ai/docs/edgetpu/models-intro/."""
|
1036
|
-
LOGGER.warning(f"{prefix}
|
1034
|
+
LOGGER.warning(f"{prefix} Edge TPU known bug https://github.com/ultralytics/ultralytics/issues/1185")
|
1037
1035
|
|
1038
1036
|
cmd = "edgetpu_compiler --version"
|
1039
1037
|
help_url = "https://coral.ai/docs/edgetpu/compiler/"
|
@@ -1094,7 +1092,7 @@ class Exporter:
|
|
1094
1092
|
subprocess.run(cmd, shell=True)
|
1095
1093
|
|
1096
1094
|
if " " in f:
|
1097
|
-
LOGGER.warning(f"{prefix}
|
1095
|
+
LOGGER.warning(f"{prefix} your model may not work correctly with spaces in path '{f}'.")
|
1098
1096
|
|
1099
1097
|
# Add metadata
|
1100
1098
|
yaml_save(Path(f) / "metadata.yaml", self.metadata) # add metadata.yaml
|
ultralytics/engine/model.py
CHANGED
@@ -21,7 +21,6 @@ from ultralytics.utils import (
|
|
21
21
|
SETTINGS,
|
22
22
|
callbacks,
|
23
23
|
checks,
|
24
|
-
emojis,
|
25
24
|
yaml_load,
|
26
25
|
)
|
27
26
|
|
@@ -528,7 +527,7 @@ class Model(torch.nn.Module):
|
|
528
527
|
"""
|
529
528
|
if source is None:
|
530
529
|
source = ASSETS
|
531
|
-
LOGGER.warning(f"
|
530
|
+
LOGGER.warning(f"'source' is missing. Using 'source={source}'.")
|
532
531
|
|
533
532
|
is_cli = (ARGV[0].endswith("yolo") or ARGV[0].endswith("ultralytics")) and any(
|
534
533
|
x in ARGV for x in ("predict", "track", "mode=predict", "mode=track")
|
@@ -766,7 +765,7 @@ class Model(torch.nn.Module):
|
|
766
765
|
self._check_is_pytorch_model()
|
767
766
|
if hasattr(self.session, "model") and self.session.model.id: # Ultralytics HUB session with loaded model
|
768
767
|
if any(kwargs):
|
769
|
-
LOGGER.warning("
|
768
|
+
LOGGER.warning("using HUB training arguments, ignoring local training arguments.")
|
770
769
|
kwargs = self.session.train_args # overwrite kwargs
|
771
770
|
|
772
771
|
checks.check_pip_update_available()
|
@@ -1082,9 +1081,7 @@ class Model(torch.nn.Module):
|
|
1082
1081
|
except Exception as e:
|
1083
1082
|
name = self.__class__.__name__
|
1084
1083
|
mode = inspect.stack()[1][3] # get the function name.
|
1085
|
-
raise NotImplementedError(
|
1086
|
-
emojis(f"WARNING ⚠️ '{name}' model does not support '{mode}' mode for '{self.task}' task yet.")
|
1087
|
-
) from e
|
1084
|
+
raise NotImplementedError(f"'{name}' model does not support '{mode}' mode for '{self.task}' task.") from e
|
1088
1085
|
|
1089
1086
|
@property
|
1090
1087
|
def task_map(self) -> dict:
|
ultralytics/engine/predictor.py
CHANGED
@@ -51,7 +51,7 @@ from ultralytics.utils.files import increment_path
|
|
51
51
|
from ultralytics.utils.torch_utils import select_device, smart_inference_mode
|
52
52
|
|
53
53
|
STREAM_WARNING = """
|
54
|
-
|
54
|
+
inference results will accumulate in RAM unless `stream=True` is passed, causing potential out-of-memory
|
55
55
|
errors for large sources or long-running streams and videos. See https://docs.ultralytics.com/modes/predict/ for help.
|
56
56
|
|
57
57
|
Example:
|
@@ -302,7 +302,9 @@ class BasePredictor:
|
|
302
302
|
|
303
303
|
# Warmup model
|
304
304
|
if not self.done_warmup:
|
305
|
-
self.model.warmup(
|
305
|
+
self.model.warmup(
|
306
|
+
imgsz=(1 if self.model.pt or self.model.triton else self.dataset.bs, self.model.ch, *self.imgsz)
|
307
|
+
)
|
306
308
|
self.done_warmup = True
|
307
309
|
|
308
310
|
self.seen, self.windows, self.batch = 0, [], None
|
@@ -361,7 +363,7 @@ class BasePredictor:
|
|
361
363
|
t = tuple(x.t / self.seen * 1e3 for x in profilers) # speeds per image
|
362
364
|
LOGGER.info(
|
363
365
|
f"Speed: %.1fms preprocess, %.1fms inference, %.1fms postprocess per image at shape "
|
364
|
-
f"{(min(self.args.batch, self.seen), 3, *im.shape[2:])}" % t
|
366
|
+
f"{(min(self.args.batch, self.seen), getattr(self.model, 'ch', 3), *im.shape[2:])}" % t
|
365
367
|
)
|
366
368
|
if self.args.save or self.args.save_txt or self.args.save_crop:
|
367
369
|
nl = len(list(self.save_dir.glob("labels/*.txt"))) # number of labels
|
ultralytics/engine/results.py
CHANGED
@@ -569,9 +569,9 @@ class Results(SimpleClass):
|
|
569
569
|
|
570
570
|
# Plot Classify results
|
571
571
|
if pred_probs is not None and show_probs:
|
572
|
-
text = "
|
572
|
+
text = "\n".join(f"{names[j] if names else j} {pred_probs.data[j]:.2f}" for j in pred_probs.top5)
|
573
573
|
x = round(self.orig_shape[0] * 0.03)
|
574
|
-
annotator.text([x, x], text, txt_color=txt_color)
|
574
|
+
annotator.text([x, x], text, txt_color=txt_color, box_color=(64, 64, 64, 128)) # RGBA box
|
575
575
|
|
576
576
|
# Plot Pose results
|
577
577
|
if self.keypoints is not None:
|
@@ -590,7 +590,7 @@ class Results(SimpleClass):
|
|
590
590
|
|
591
591
|
# Save results
|
592
592
|
if save:
|
593
|
-
annotator.save(filename)
|
593
|
+
annotator.save(filename or f"results_{Path(self.path).name}")
|
594
594
|
|
595
595
|
return annotator.im if pil else annotator.result()
|
596
596
|
|
@@ -752,10 +752,10 @@ class Results(SimpleClass):
|
|
752
752
|
>>> result.save_crop(save_dir="path/to/crops", file_name="detection")
|
753
753
|
"""
|
754
754
|
if self.probs is not None:
|
755
|
-
LOGGER.warning("
|
755
|
+
LOGGER.warning("Classify task do not support `save_crop`.")
|
756
756
|
return
|
757
757
|
if self.obb is not None:
|
758
|
-
LOGGER.warning("
|
758
|
+
LOGGER.warning("OBB task do not support `save_crop`.")
|
759
759
|
return
|
760
760
|
for d in self.boxes:
|
761
761
|
save_one_box(
|
@@ -942,7 +942,7 @@ class Results(SimpleClass):
|
|
942
942
|
|
943
943
|
def tojson(self, normalize=False, decimals=5):
|
944
944
|
"""Deprecated version of to_json()."""
|
945
|
-
LOGGER.warning("
|
945
|
+
LOGGER.warning("'result.tojson()' is deprecated, replace with 'result.to_json()'.")
|
946
946
|
return self.to_json(normalize, decimals)
|
947
947
|
|
948
948
|
def to_json(self, normalize=False, decimals=5):
|
@@ -1005,7 +1005,7 @@ class Results(SimpleClass):
|
|
1005
1005
|
# Convert results to a list of dictionaries
|
1006
1006
|
data = self.summary(normalize=normalize, decimals=decimals)
|
1007
1007
|
if len(data) == 0:
|
1008
|
-
LOGGER.warning("
|
1008
|
+
LOGGER.warning("No results to save to SQL. Results dict is empty.")
|
1009
1009
|
return
|
1010
1010
|
|
1011
1011
|
# Connect to the SQLite database
|
ultralytics/engine/trainer.py
CHANGED
@@ -188,12 +188,11 @@ class BaseTrainer:
|
|
188
188
|
if world_size > 1 and "LOCAL_RANK" not in os.environ:
|
189
189
|
# Argument checks
|
190
190
|
if self.args.rect:
|
191
|
-
LOGGER.warning("
|
191
|
+
LOGGER.warning("'rect=True' is incompatible with Multi-GPU training, setting 'rect=False'")
|
192
192
|
self.args.rect = False
|
193
193
|
if self.args.batch < 1.0:
|
194
194
|
LOGGER.warning(
|
195
|
-
"
|
196
|
-
"default 'batch=16'"
|
195
|
+
"'batch<1' for AutoBatch is incompatible with Multi-GPU training, setting default 'batch=16'"
|
197
196
|
)
|
198
197
|
self.args.batch = 16
|
199
198
|
|
@@ -256,8 +255,8 @@ class BaseTrainer:
|
|
256
255
|
LOGGER.info(f"Freezing layer '{k}'")
|
257
256
|
v.requires_grad = False
|
258
257
|
elif not v.requires_grad and v.dtype.is_floating_point: # only floating point Tensor can require gradients
|
259
|
-
LOGGER.
|
260
|
-
f"
|
258
|
+
LOGGER.warning(
|
259
|
+
f"setting 'requires_grad=True' for frozen layer '{k}'. "
|
261
260
|
"See ultralytics.engine.trainer for customization of frozen layers."
|
262
261
|
)
|
263
262
|
v.requires_grad = True
|