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
         |