ultralytics 8.3.111__py3-none-any.whl → 8.3.113__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.
Files changed (71) hide show
  1. ultralytics/__init__.py +1 -1
  2. ultralytics/cfg/__init__.py +14 -16
  3. ultralytics/cfg/datasets/coco8-multispectral.yaml +104 -0
  4. ultralytics/cfg/datasets/dota8-multispectral.yaml +38 -0
  5. ultralytics/data/augment.py +17 -7
  6. ultralytics/data/base.py +24 -26
  7. ultralytics/data/converter.py +54 -3
  8. ultralytics/data/dataset.py +5 -5
  9. ultralytics/data/loaders.py +8 -10
  10. ultralytics/data/split.py +123 -0
  11. ultralytics/data/utils.py +34 -52
  12. ultralytics/engine/exporter.py +22 -24
  13. ultralytics/engine/model.py +3 -6
  14. ultralytics/engine/predictor.py +8 -4
  15. ultralytics/engine/results.py +7 -7
  16. ultralytics/engine/trainer.py +4 -5
  17. ultralytics/engine/tuner.py +1 -1
  18. ultralytics/engine/validator.py +4 -4
  19. ultralytics/hub/auth.py +1 -1
  20. ultralytics/hub/session.py +3 -3
  21. ultralytics/models/rtdetr/train.py +1 -22
  22. ultralytics/models/sam/modules/sam.py +2 -1
  23. ultralytics/models/yolo/classify/train.py +1 -1
  24. ultralytics/models/yolo/detect/train.py +2 -2
  25. ultralytics/models/yolo/detect/val.py +1 -1
  26. ultralytics/models/yolo/obb/train.py +1 -1
  27. ultralytics/models/yolo/pose/predict.py +1 -1
  28. ultralytics/models/yolo/pose/train.py +4 -2
  29. ultralytics/models/yolo/pose/val.py +1 -1
  30. ultralytics/models/yolo/segment/train.py +1 -1
  31. ultralytics/models/yolo/segment/val.py +1 -1
  32. ultralytics/models/yolo/world/train.py +1 -1
  33. ultralytics/models/yolo/world/train_world.py +1 -0
  34. ultralytics/models/yolo/yoloe/train.py +2 -2
  35. ultralytics/models/yolo/yoloe/train_seg.py +2 -2
  36. ultralytics/nn/autobackend.py +16 -6
  37. ultralytics/nn/tasks.py +11 -11
  38. ultralytics/solutions/instance_segmentation.py +1 -1
  39. ultralytics/solutions/object_blurrer.py +1 -1
  40. ultralytics/solutions/object_cropper.py +2 -2
  41. ultralytics/solutions/parking_management.py +1 -1
  42. ultralytics/solutions/region_counter.py +3 -2
  43. ultralytics/solutions/security_alarm.py +1 -1
  44. ultralytics/solutions/solutions.py +3 -6
  45. ultralytics/trackers/byte_tracker.py +1 -1
  46. ultralytics/trackers/utils/gmc.py +4 -4
  47. ultralytics/utils/__init__.py +28 -21
  48. ultralytics/utils/autobatch.py +4 -4
  49. ultralytics/utils/benchmarks.py +8 -8
  50. ultralytics/utils/callbacks/clearml.py +1 -1
  51. ultralytics/utils/callbacks/comet.py +5 -5
  52. ultralytics/utils/callbacks/dvc.py +1 -1
  53. ultralytics/utils/callbacks/mlflow.py +2 -1
  54. ultralytics/utils/callbacks/neptune.py +1 -1
  55. ultralytics/utils/callbacks/tensorboard.py +7 -9
  56. ultralytics/utils/checks.py +20 -26
  57. ultralytics/utils/downloads.py +4 -4
  58. ultralytics/utils/export.py +1 -1
  59. ultralytics/utils/metrics.py +1 -1
  60. ultralytics/utils/ops.py +1 -1
  61. ultralytics/utils/patches.py +8 -1
  62. ultralytics/utils/plotting.py +27 -29
  63. ultralytics/utils/tal.py +1 -1
  64. ultralytics/utils/torch_utils.py +5 -5
  65. ultralytics/utils/tuner.py +2 -2
  66. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.dist-info}/METADATA +1 -1
  67. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.dist-info}/RECORD +71 -68
  68. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.dist-info}/WHEEL +1 -1
  69. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.dist-info}/entry_points.txt +0 -0
  70. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.dist-info}/licenses/LICENSE +0 -0
  71. {ultralytics-8.3.111.dist-info → ultralytics-8.3.113.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}WARNING ⚠️ Image speed checks: No files to check")
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}WARNING ⚠️ Image speed checks: failed to access files")
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}WARNING ⚠️ Slow image access detected ({ping_msg}{speed_msg}{size_msg}). "
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}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved"
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}WARNING ⚠️ {im_file}: ignoring corrupt image/label: {e}"
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}WARNING ⚠️ {im_file}: corrupt JPEG restored and saved"
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}WARNING ⚠️ {im_file}: {nl - len(i)} duplicate labels removed"
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}WARNING ⚠️ {im_file}: ignoring corrupt image/label: {e}"
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.info("WARNING ⚠️ renaming data YAML 'validation' key to 'val' to match YOLO format.")
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
- m = f"\nDataset '{name}' images not found ⚠️, missing path '{[x for x in val if not x.exists()][0]}'"
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.warning(f"\nDataset not found ⚠️, missing path {data_dir}, attempting download...")
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
- s = f"Dataset download success ✅ ({time.time() - t:.1f}s), saved to {colorstr('bold', data_dir)}\n"
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("WARNING ⚠️ Dataset 'split=val' not found, using 'split=test' instead.")
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("WARNING ⚠️ Dataset 'split=test' not found, using 'split=val' instead.")
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(emojis(f"{dataset} '{k}:' no training images found"))
552
+ raise FileNotFoundError(f"{dataset} '{k}:' no training images found")
540
553
  else:
541
- LOGGER.warning(f"{prefix} found {nf} images in {nd} classes: WARNING ⚠️ no images found")
554
+ LOGGER.warning(f"{prefix} found {nf} images in {nd} classes (no images found)")
542
555
  elif nd != nc:
543
- LOGGER.warning(f"{prefix} found {nf} images in {nd} classes: ERROR ❌️ requires {nc} classes, not {nd}")
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.info(f"WARNING ⚠️ HUB ops PIL failure {f}: {e}")
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}WARNING ⚠️ Cache directory {path.parent} is not writeable, cache not saved.")
773
+ LOGGER.warning(f"{prefix}Cache directory {path.parent} is not writeable, cache not saved.")
@@ -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 {dt.t:.1f}s: {e}")
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"WARNING ⚠️ Invalid export format='{fmt}', updating to format='{matches[0]}'")
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("WARNING ⚠️ TensorRT requires GPU export, automatically assigning device=0")
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("WARNING ⚠️ IMX export requires int8=True, setting int8=True.")
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("WARNING ⚠️ half=True and int8=True are mutually exclusive, setting half=False.")
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("WARNING ⚠️ half=True only compatible with GPU export, i.e. use device=0")
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
- "WARNING ⚠️ Rockchip RKNN export requires a missing 'name' arg for processor type. "
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("WARNING ⚠️ 'nms=True' is not available for end2end models. Forcing 'nms=False'.")
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("WARNING ⚠️ Edge TPU export requires batch size 1, setting batch=1.")
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
- "WARNING ⚠️ YOLOWorld (original version) export is not supported to any format.\n"
347
- "WARNING ⚠️ YOLOWorldv2 models (i.e. 'yolov8s-worldv2.pt') only support export to "
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
- "WARNING ⚠️ INT8 export requires a missing 'data' arg for calibration. "
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} WARNING ⚠️ >300 images recommended for INT8 calibration, found {n} images.")
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} WARNING ⚠️ PNNX not found. Attempting to download binary file from "
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} WARNING ⚠️ PNNX GitHub assets not found: {e}, using default {asset}")
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} WARNING ⚠️ 'nms=True' is only available for Detect models like 'yolo11n.pt'.")
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} WARNING ⚠️ CoreML export to *.mlpackage failed ({e}), reverting to *.mlmodel export. "
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} WARNING ⚠️ Edge TPU known bug https://github.com/ultralytics/ultralytics/issues/1185")
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} WARNING ⚠️ your model may not work correctly with spaces in path '{f}'.")
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
@@ -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"WARNING ⚠️ 'source' is missing. Using 'source={source}'.")
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("WARNING ⚠️ using HUB training arguments, ignoring local training arguments.")
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:
@@ -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
- WARNING ⚠️ inference results will accumulate in RAM unless `stream=True` is passed, causing potential out-of-memory
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:
@@ -151,7 +151,9 @@ class BasePredictor:
151
151
  not_tensor = not isinstance(im, torch.Tensor)
152
152
  if not_tensor:
153
153
  im = np.stack(self.pre_transform(im))
154
- im = im[..., ::-1].transpose((0, 3, 1, 2)) # BGR to RGB, BHWC to BCHW, (n, 3, h, w)
154
+ if im.shape[-1] == 3:
155
+ im = im[..., ::-1] # BGR to RGB
156
+ im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
155
157
  im = np.ascontiguousarray(im) # contiguous
156
158
  im = torch.from_numpy(im)
157
159
 
@@ -302,7 +304,9 @@ class BasePredictor:
302
304
 
303
305
  # Warmup model
304
306
  if not self.done_warmup:
305
- self.model.warmup(imgsz=(1 if self.model.pt or self.model.triton else self.dataset.bs, 3, *self.imgsz))
307
+ self.model.warmup(
308
+ imgsz=(1 if self.model.pt or self.model.triton else self.dataset.bs, self.model.ch, *self.imgsz)
309
+ )
306
310
  self.done_warmup = True
307
311
 
308
312
  self.seen, self.windows, self.batch = 0, [], None
@@ -361,7 +365,7 @@ class BasePredictor:
361
365
  t = tuple(x.t / self.seen * 1e3 for x in profilers) # speeds per image
362
366
  LOGGER.info(
363
367
  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
368
+ f"{(min(self.args.batch, self.seen), getattr(self.model, 'ch', 3), *im.shape[2:])}" % t
365
369
  )
366
370
  if self.args.save or self.args.save_txt or self.args.save_crop:
367
371
  nl = len(list(self.save_dir.glob("labels/*.txt"))) # number of labels
@@ -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 = ",\n".join(f"{names[j] if names else j} {pred_probs.data[j]:.2f}" for j in pred_probs.top5)
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("WARNING ⚠️ Classify task do not support `save_crop`.")
755
+ LOGGER.warning("Classify task do not support `save_crop`.")
756
756
  return
757
757
  if self.obb is not None:
758
- LOGGER.warning("WARNING ⚠️ OBB task do not support `save_crop`.")
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("WARNING ⚠️ 'result.tojson()' is deprecated, replace with 'result.to_json()'.")
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("⚠️ No results to save to SQL. Results dict is empty")
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