From fb1ae9bfad6f92aa2751687460afda7b7dd5ca94 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Wed, 16 Aug 2023 19:00:03 +0200 Subject: [PATCH] Fix `LoadStreams` final frame bug (#4387) Co-authored-by: Nadim Bou Alwan <64587372+nadinator@users.noreply.github.com> --- docs/guides/kfold-cross-validation.md | 2 +- setup.cfg | 2 +- tests/conftest.py | 25 +++++++++ tests/test_cli.py | 17 +----- tests/test_python.py | 76 +++++++++++++++++---------- ultralytics/cfg/__init__.py | 2 +- ultralytics/data/converter.py | 9 ++-- ultralytics/data/loaders.py | 9 ++-- ultralytics/engine/exporter.py | 1 + ultralytics/nn/modules/conv.py | 9 ---- ultralytics/utils/checks.py | 12 +++-- 11 files changed, 97 insertions(+), 67 deletions(-) diff --git a/docs/guides/kfold-cross-validation.md b/docs/guides/kfold-cross-validation.md index 41854d4..2accea6 100644 --- a/docs/guides/kfold-cross-validation.md +++ b/docs/guides/kfold-cross-validation.md @@ -246,7 +246,7 @@ fold_lbl_distrb.to_csv(save_path / "kfold_label_distribution.csv") results = {} for k in range(ksplit): dataset_yaml = ds_yamls[k] - results = model.train(data=dataset_yaml, *args, **kwargs) # Include any training arguments + model.train(data=dataset_yaml, *args, **kwargs) # Include any training arguments results[k] = model.metrics # save output metrics for further analysis ``` diff --git a/setup.cfg b/setup.cfg index 6ce9c00..a7d16ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ norecursedirs = build addopts = --doctest-modules - --durations=25 + --durations=30 --color=yes [coverage:run] diff --git a/tests/conftest.py b/tests/conftest.py index 153dc86..b418863 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,14 @@ # Ultralytics YOLO 🚀, AGPL-3.0 license +import shutil +from pathlib import Path + import pytest +from ultralytics.utils import ROOT + +TMP = (ROOT / '../tests/tmp').resolve() # temp directory for test files + def pytest_addoption(parser): parser.addoption('--runslow', action='store_true', default=False, help='run slow tests') @@ -19,3 +26,21 @@ def pytest_collection_modifyitems(config, items): for item in items: if 'slow' in item.keywords: item.add_marker(skip_slow) + + +def pytest_sessionstart(session): + """ + Called after the 'Session' object has been created and before performing test collection. + """ + shutil.rmtree(TMP, ignore_errors=True) # delete any existing tests/tmp directory + TMP.mkdir(parents=True, exist_ok=True) # create a new empty directory + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + # Remove files + for file in ['bus.jpg', 'decelera_landscape_min.mov']: + Path(file).unlink(missing_ok=True) + + # Remove directories + for directory in ['.pytest_cache/', TMP]: + shutil.rmtree(directory, ignore_errors=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 24dc4d2..88888a1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -from ultralytics.utils import ONLINE, ROOT, SETTINGS +from ultralytics.utils import ROOT, SETTINGS WEIGHT_DIR = Path(SETTINGS['weights_dir']) TASK_ARGS = [ @@ -30,7 +30,6 @@ def test_special_modes(): run('yolo checks') run('yolo version') run('yolo settings reset') - run('yolo copy-cfg') run('yolo cfg') @@ -49,28 +48,14 @@ def test_predict(task, model, data): run(f"yolo predict model={WEIGHT_DIR / model}.pt source={ROOT / 'assets'} imgsz=32 save save_crop save_txt") -@pytest.mark.skipif(not ONLINE, reason='environment is offline') -@pytest.mark.parametrize('task,model,data', TASK_ARGS) -def test_predict_online(task, model, data): - mode = 'track' if task in ('detect', 'segment', 'pose') else 'predict' # mode for video inference - model = WEIGHT_DIR / model - run(f'yolo predict model={model}.pt source=https://ultralytics.com/images/bus.jpg imgsz=32') - run(f'yolo {mode} model={model}.pt source=https://ultralytics.com/assets/decelera_landscape_min.mov imgsz=96') - - # Run Python YouTube tracking because CLI is broken. TODO: fix CLI YouTube - # run(f'yolo {mode} model={model}.pt source=https://youtu.be/G17sBkb38XQ imgsz=32 tracker=bytetrack.yaml') - - @pytest.mark.parametrize('model,format', EXPORT_ARGS) def test_export(model, format): run(f'yolo export model={WEIGHT_DIR / model}.pt format={format} imgsz=32') -# Test SAM, RTDETR Models def test_rtdetr(task='detect', model='yolov8n-rtdetr.yaml', data='coco8.yaml'): # Warning: MUST use imgsz=640 run(f'yolo train {task} model={model} data={data} imgsz=640 epochs=1 cache=disk') - run(f'yolo val {task} model={model} data={data} imgsz=640') run(f"yolo predict {task} model={model} source={ROOT / 'assets/bus.jpg'} imgsz=640 save save_crop save_txt") diff --git a/tests/test_python.py b/tests/test_python.py index da59a81..b97a933 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1,17 +1,20 @@ # Ultralytics YOLO 🚀, AGPL-3.0 license import shutil +from copy import copy from pathlib import Path import cv2 import numpy as np +import pytest import torch from PIL import Image from torchvision.transforms import ToTensor from ultralytics import RTDETR, YOLO from ultralytics.data.build import load_inference_source -from ultralytics.utils import LINUX, MACOS, ONLINE, ROOT, SETTINGS +from ultralytics.utils import DEFAULT_CFG, LINUX, ONLINE, ROOT, SETTINGS +from ultralytics.utils.downloads import download from ultralytics.utils.torch_utils import TORCH_1_9 WEIGHTS_DIR = Path(SETTINGS['weights_dir']) @@ -19,13 +22,6 @@ MODEL = WEIGHTS_DIR / 'path with spaces' / 'yolov8n.pt' # test spaces in path CFG = 'yolov8n.yaml' SOURCE = ROOT / 'assets/bus.jpg' TMP = (ROOT / '../tests/tmp').resolve() # temp directory for test files -SOURCE_GREYSCALE = Path(f'{SOURCE.parent / SOURCE.stem}_greyscale.jpg') -SOURCE_RGBA = Path(f'{SOURCE.parent / SOURCE.stem}_4ch.png') - -# Convert SOURCE to greyscale and 4-ch -im = Image.open(SOURCE) -im.convert('L').save(SOURCE_GREYSCALE) # greyscale -im.convert('RGBA').save(SOURCE_RGBA) # 4-ch PNG with alpha def test_model_forward(): @@ -84,16 +80,32 @@ def test_predict_img(): def test_predict_grey_and_4ch(): + # Convert SOURCE to greyscale and 4-ch + im = Image.open(SOURCE) + source_greyscale = Path(f'{SOURCE.parent / SOURCE.stem}_greyscale.jpg') + source_rgba = Path(f'{SOURCE.parent / SOURCE.stem}_4ch.png') + im.convert('L').save(source_greyscale) # greyscale + im.convert('RGBA').save(source_rgba) # 4-ch PNG with alpha + + # Inference model = YOLO(MODEL) - for f in SOURCE_RGBA, SOURCE_GREYSCALE: + for f in source_rgba, source_greyscale: for source in Image.open(f), cv2.imread(str(f)), f: model(source, save=True, verbose=True, imgsz=32) + # Cleanup + source_greyscale.unlink() + source_rgba.unlink() + +@pytest.mark.skipif(not ONLINE, reason='environment is offline') def test_track_stream(): # Test YouTube streaming inference (short 10 frame video) with non-default ByteTrack tracker + # imgsz=160 required for tracking for higher confidence and better matches model = YOLO(MODEL) - model.track('https://youtu.be/G17sBkb38XQ', imgsz=96, tracker='bytetrack.yaml') + model.predict('https://youtu.be/G17sBkb38XQ', imgsz=96) + model.track('https://ultralytics.com/assets/decelera_portrait_min.mov', imgsz=160, tracker='bytetrack.yaml') + model.track('https://ultralytics.com/assets/decelera_portrait_min.mov', imgsz=160, tracker='botsort.yaml') def test_val(): @@ -101,13 +113,6 @@ def test_val(): model.val(data='coco8.yaml', imgsz=32) -def test_amp(): - if torch.cuda.is_available(): - from ultralytics.utils.checks import check_amp - model = YOLO(MODEL).model.cuda() - assert check_amp(model) - - def test_train_scratch(): model = YOLO(CFG) model.train(data='coco8.yaml', epochs=1, imgsz=32, cache='disk', batch=-1) # test disk caching with AutoBatch @@ -133,10 +138,9 @@ def test_export_onnx(): def test_export_openvino(): - if not MACOS: - model = YOLO(MODEL) - f = model.export(format='openvino') - YOLO(f)(SOURCE) # exported model inference + model = YOLO(MODEL) + f = model.export(format='openvino') + YOLO(f)(SOURCE) # exported model inference def test_export_coreml(): # sourcery skip: move-assign @@ -173,7 +177,7 @@ def test_all_model_yamls(): for m in (ROOT / 'cfg' / 'models').rglob('*.yaml'): if 'rtdetr' in m.name: if TORCH_1_9: # torch<=1.8 issue - TypeError: __init__() got an unexpected keyword argument 'batch_first' - RTDETR(m.name) + RTDETR(m.name)(SOURCE, imgsz=640) else: YOLO(m.name) @@ -225,17 +229,14 @@ def test_results(): print(getattr(r, k)) +@pytest.mark.skipif(not ONLINE, reason='environment is offline') def test_data_utils(): # Test functions in ultralytics/data/utils.py from ultralytics.data.utils import HUBDatasetStats, autosplit, zip_directory - from ultralytics.utils.downloads import download # from ultralytics.utils.files import WorkingDirectory # with WorkingDirectory(ROOT.parent / 'tests'): - shutil.rmtree(TMP, ignore_errors=True) - TMP.mkdir(parents=True) - download('https://github.com/ultralytics/hub/raw/master/example_datasets/coco8.zip', unzip=False) shutil.move('coco8.zip', TMP) stats = HUBDatasetStats(TMP / 'coco8.zip', task='detect') @@ -244,4 +245,25 @@ def test_data_utils(): autosplit(TMP / 'coco8') zip_directory(TMP / 'coco8/images/val') # zip - shutil.rmtree(TMP) + + +@pytest.mark.skipif(not ONLINE, reason='environment is offline') +def test_data_converter(): + # Test dataset converters + from ultralytics.data.converter import convert_coco + + file = 'instances_val2017.json' + download(f'https://github.com/ultralytics/yolov5/releases/download/v1.0/{file}') + shutil.move(file, TMP) + convert_coco(labels_dir=TMP, use_segments=True, use_keypoints=False, cls91to80=True) + + +def test_events(): + # Test event sending + from ultralytics.hub.utils import Events + + events = Events() + events.enabled = True + cfg = copy(DEFAULT_CFG) # does not require deepcopy + cfg.mode = 'test' + events(cfg) diff --git a/ultralytics/cfg/__init__.py b/ultralytics/cfg/__init__.py index eac8174..d189f56 100644 --- a/ultralytics/cfg/__init__.py +++ b/ultralytics/cfg/__init__.py @@ -442,5 +442,5 @@ def copy_default_cfg(): if __name__ == '__main__': - # Example Usage: entrypoint(debug='yolo predict model=yolov8n.pt') + # Example: entrypoint(debug='yolo predict model=yolov8n.pt') entrypoint(debug='') diff --git a/ultralytics/data/converter.py b/ultralytics/data/converter.py index 3256885..218b5fa 100644 --- a/ultralytics/data/converter.py +++ b/ultralytics/data/converter.py @@ -36,11 +36,12 @@ def convert_coco(labels_dir='../coco/annotations/', use_segments=False, use_keyp use_keypoints (bool, optional): Whether to include keypoint annotations in the output. cls91to80 (bool, optional): Whether to map 91 COCO class IDs to the corresponding 80 COCO class IDs. - Raises: - FileNotFoundError: If the labels_dir path does not exist. + Example: + ```python + from ultralytics.data.converter import convert_coco - Example Usage: - convert_coco(labels_dir='../coco/annotations/', use_segments=True, use_keypoints=True, cls91to80=True) + convert_coco('../datasets/coco/annotations/', use_segments=True, use_keypoints=False, cls91to80=True) + ``` Output: Generates output files in the specified output directory. diff --git a/ultralytics/data/loaders.py b/ultralytics/data/loaders.py index fdf6167..13e8f47 100644 --- a/ultralytics/data/loaders.py +++ b/ultralytics/data/loaders.py @@ -79,19 +79,18 @@ class LoadStreams: def update(self, i, cap, stream): """Read stream `i` frames in daemon thread.""" n, f = 0, self.frames[i] # frame number, frame array - while self.running and cap.isOpened() and n < f: + while self.running and cap.isOpened() and n < (f - 1): # Only read a new frame if the buffer is empty if not self.imgs[i]: n += 1 cap.grab() # .read() = .grab() followed by .retrieve() if n % self.vid_stride == 0: success, im = cap.retrieve() - if success: - self.imgs[i].append(im) # add image to buffer - else: + if not success: + im = np.zeros(self.shape[i], dtype=np.uint8) LOGGER.warning('WARNING ⚠️ Video stream unresponsive, please check your IP camera connection.') - self.imgs[i].append(np.zeros(self.shape[i])) cap.open(stream) # re-open stream if signal was lost + self.imgs[i].append(im) # add image to buffer else: time.sleep(0.01) # wait until the buffer is empty diff --git a/ultralytics/engine/exporter.py b/ultralytics/engine/exporter.py index 275ba88..6800ea5 100644 --- a/ultralytics/engine/exporter.py +++ b/ultralytics/engine/exporter.py @@ -463,6 +463,7 @@ class Exporter: yaml_save(f / 'metadata.yaml', self.metadata) # add metadata.yaml return str(f), None + @try_export def export_coreml(self, prefix=colorstr('CoreML:')): """YOLOv8 CoreML export.""" mlmodel = self.args.format.lower() == 'mlmodel' # legacy *.mlmodel export format requested diff --git a/ultralytics/nn/modules/conv.py b/ultralytics/nn/modules/conv.py index 6232a78..9577ba0 100644 --- a/ultralytics/nn/modules/conv.py +++ b/ultralytics/nn/modules/conv.py @@ -175,15 +175,6 @@ class RepConv(nn.Module): kernelid, biasid = self._fuse_bn_tensor(self.bn) return kernel3x3 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernelid, bias3x3 + bias1x1 + biasid - def _avg_to_3x3_tensor(self, avgp): - channels = self.c1 - groups = self.g - kernel_size = avgp.kernel_size - input_dim = channels // groups - k = torch.zeros((channels, input_dim, kernel_size, kernel_size)) - k[np.arange(channels), np.tile(np.arange(input_dim), groups), :, :] = 1.0 / kernel_size ** 2 - return k - def _pad_1x1_to_3x3_tensor(self, kernel1x1): if kernel1x1 is None: return 0 diff --git a/ultralytics/utils/checks.py b/ultralytics/utils/checks.py index 312cfcc..aef758e 100644 --- a/ultralytics/utils/checks.py +++ b/ultralytics/utils/checks.py @@ -437,11 +437,17 @@ def check_amp(model): Args: model (nn.Module): A YOLOv8 model instance. + Example: + ```python + from ultralytics import YOLO + from ultralytics.utils.checks import check_amp + + model = YOLO('yolov8n.pt').model.cuda() + check_amp(model) + ``` + Returns: (bool): Returns True if the AMP functionality works correctly with YOLOv8 model, else False. - - Raises: - AssertionError: If the AMP checks fail, indicating anomalies with the AMP functionality on the system. """ device = next(model.parameters()).device # get model device if device.type in ('cpu', 'mps'):