dataset.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. # Ultralytics YOLO 🚀, AGPL-3.0 license
  2. import contextlib
  3. from itertools import repeat
  4. from multiprocessing.pool import ThreadPool
  5. from pathlib import Path
  6. import cv2
  7. import numpy as np
  8. import torch
  9. import torchvision
  10. from tqdm import tqdm
  11. from ultralytics.utils import LOCAL_RANK, NUM_THREADS, TQDM_BAR_FORMAT, colorstr, is_dir_writeable
  12. from .augment import Compose, Format, Instances, LetterBox, classify_albumentations, classify_transforms, v8_transforms
  13. from .base import BaseDataset
  14. from .utils import HELP_URL, LOGGER, get_hash, img2label_paths, verify_image, verify_image_label
  15. # Ultralytics dataset *.cache version, >= 1.0.0 for YOLOv8
  16. DATASET_CACHE_VERSION = '1.0.3'
  17. class YOLODataset(BaseDataset):
  18. """
  19. Dataset class for loading object detection and/or segmentation labels in YOLO format.
  20. Args:
  21. data (dict, optional): A dataset YAML dictionary. Defaults to None.
  22. use_segments (bool, optional): If True, segmentation masks are used as labels. Defaults to False.
  23. use_keypoints (bool, optional): If True, keypoints are used as labels. Defaults to False.
  24. Returns:
  25. (torch.utils.data.Dataset): A PyTorch dataset object that can be used for training an object detection model.
  26. """
  27. def __init__(self, *args, data=None, use_segments=False, use_keypoints=False, **kwargs):
  28. self.use_segments = use_segments
  29. self.use_keypoints = use_keypoints
  30. self.data = data
  31. assert not (self.use_segments and self.use_keypoints), 'Can not use both segments and keypoints.'
  32. super().__init__(*args, **kwargs)
  33. def cache_labels(self, path=Path('./labels.cache')):
  34. """Cache dataset labels, check images and read shapes.
  35. Args:
  36. path (Path): path where to save the cache file (default: Path('./labels.cache')).
  37. Returns:
  38. (dict): labels.
  39. """
  40. x = {'labels': []}
  41. nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages
  42. desc = f'{self.prefix}Scanning {path.parent / path.stem}...'
  43. total = len(self.im_files)
  44. nkpt, ndim = self.data.get('kpt_shape', (0, 0))
  45. if self.use_keypoints and (nkpt <= 0 or ndim not in (2, 3)):
  46. raise ValueError("'kpt_shape' in data.yaml missing or incorrect. Should be a list with [number of "
  47. "keypoints, number of dims (2 for x,y or 3 for x,y,visible)], i.e. 'kpt_shape: [17, 3]'")
  48. with ThreadPool(NUM_THREADS) as pool:
  49. results = pool.imap(func=verify_image_label,
  50. iterable=zip(self.im_files, self.label_files, repeat(self.prefix),
  51. repeat(self.use_keypoints), repeat(len(self.data['names'])), repeat(nkpt),
  52. repeat(ndim)))
  53. pbar = tqdm(results, desc=desc, total=total, bar_format=TQDM_BAR_FORMAT)
  54. for im_file, lb, shape, segments, keypoint, nm_f, nf_f, ne_f, nc_f, msg in pbar:
  55. nm += nm_f
  56. nf += nf_f
  57. ne += ne_f
  58. nc += nc_f
  59. if im_file:
  60. x['labels'].append(
  61. dict(
  62. im_file=im_file,
  63. shape=shape,
  64. cls=lb[:, 0:1], # n, 1
  65. bboxes=lb[:, 1:], # n, 4
  66. segments=segments,
  67. keypoints=keypoint,
  68. normalized=True,
  69. bbox_format='xywh'))
  70. if msg:
  71. msgs.append(msg)
  72. pbar.desc = f'{desc} {nf} images, {nm + ne} backgrounds, {nc} corrupt'
  73. pbar.close()
  74. if msgs:
  75. LOGGER.info('\n'.join(msgs))
  76. if nf == 0:
  77. LOGGER.warning(f'{self.prefix}WARNING ⚠️ No labels found in {path}. {HELP_URL}')
  78. x['hash'] = get_hash(self.label_files + self.im_files)
  79. x['results'] = nf, nm, ne, nc, len(self.im_files)
  80. x['msgs'] = msgs # warnings
  81. save_dataset_cache_file(self.prefix, path, x)
  82. return x
  83. def get_labels(self):
  84. """Returns dictionary of labels for YOLO training."""
  85. self.label_files = img2label_paths(self.im_files)
  86. cache_path = Path(self.label_files[0]).parent.with_suffix('.cache')
  87. try:
  88. cache, exists = load_dataset_cache_file(cache_path), True # attempt to load a *.cache file
  89. assert cache['version'] == DATASET_CACHE_VERSION # matches current version
  90. assert cache['hash'] == get_hash(self.label_files + self.im_files) # identical hash
  91. except (FileNotFoundError, AssertionError, AttributeError):
  92. cache, exists = self.cache_labels(cache_path), False # run cache ops
  93. # Display cache
  94. nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupt, total
  95. if exists and LOCAL_RANK in (-1, 0):
  96. d = f'Scanning {cache_path}... {nf} images, {nm + ne} backgrounds, {nc} corrupt'
  97. tqdm(None, desc=self.prefix + d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT) # display results
  98. if cache['msgs']:
  99. LOGGER.info('\n'.join(cache['msgs'])) # display warnings
  100. if nf == 0: # number of labels found
  101. raise FileNotFoundError(f'{self.prefix}No labels found in {cache_path}, can not start training. {HELP_URL}')
  102. # Read cache
  103. [cache.pop(k) for k in ('hash', 'version', 'msgs')] # remove items
  104. labels = cache['labels']
  105. assert len(labels), f'No valid labels found, please check your dataset. {HELP_URL}'
  106. self.im_files = [lb['im_file'] for lb in labels] # update im_files
  107. # Check if the dataset is all boxes or all segments
  108. lengths = ((len(lb['cls']), len(lb['bboxes']), len(lb['segments'])) for lb in labels)
  109. len_cls, len_boxes, len_segments = (sum(x) for x in zip(*lengths))
  110. if len_segments and len_boxes != len_segments:
  111. LOGGER.warning(
  112. f'WARNING ⚠️ Box and segment counts should be equal, but got len(segments) = {len_segments}, '
  113. f'len(boxes) = {len_boxes}. To resolve this only boxes will be used and all segments will be removed. '
  114. 'To avoid this please supply either a detect or segment dataset, not a detect-segment mixed dataset.')
  115. for lb in labels:
  116. lb['segments'] = []
  117. if len_cls == 0:
  118. raise ValueError(f'All labels empty in {cache_path}, can not start training without labels. {HELP_URL}')
  119. return labels
  120. # TODO: use hyp config to set all these augmentations
  121. def build_transforms(self, hyp=None):
  122. """Builds and appends transforms to the list."""
  123. if self.augment:
  124. hyp.mosaic = hyp.mosaic if self.augment and not self.rect else 0.0
  125. hyp.mixup = hyp.mixup if self.augment and not self.rect else 0.0
  126. transforms = v8_transforms(self, self.imgsz, hyp)
  127. else:
  128. transforms = Compose([LetterBox(new_shape=(self.imgsz, self.imgsz), scaleup=False)])
  129. transforms.append(
  130. Format(bbox_format='xywh',
  131. normalize=True,
  132. return_mask=self.use_segments,
  133. return_keypoint=self.use_keypoints,
  134. batch_idx=True,
  135. mask_ratio=hyp.mask_ratio,
  136. mask_overlap=hyp.overlap_mask))
  137. return transforms
  138. def close_mosaic(self, hyp):
  139. """Sets mosaic, copy_paste and mixup options to 0.0 and builds transformations."""
  140. hyp.mosaic = 0.0 # set mosaic ratio=0.0
  141. hyp.copy_paste = 0.0 # keep the same behavior as previous v8 close-mosaic
  142. hyp.mixup = 0.0 # keep the same behavior as previous v8 close-mosaic
  143. self.transforms = self.build_transforms(hyp)
  144. def update_labels_info(self, label):
  145. """custom your label format here."""
  146. # NOTE: cls is not with bboxes now, classification and semantic segmentation need an independent cls label
  147. # we can make it also support classification and semantic segmentation by add or remove some dict keys there.
  148. bboxes = label.pop('bboxes')
  149. segments = label.pop('segments')
  150. keypoints = label.pop('keypoints', None)
  151. bbox_format = label.pop('bbox_format')
  152. normalized = label.pop('normalized')
  153. label['instances'] = Instances(bboxes, segments, keypoints, bbox_format=bbox_format, normalized=normalized)
  154. return label
  155. @staticmethod
  156. def collate_fn(batch):
  157. """Collates data samples into batches."""
  158. new_batch = {}
  159. keys = batch[0].keys()
  160. values = list(zip(*[list(b.values()) for b in batch]))
  161. for i, k in enumerate(keys):
  162. value = values[i]
  163. if k == 'img':
  164. value = torch.stack(value, 0)
  165. if k in ['masks', 'keypoints', 'bboxes', 'cls']:
  166. value = torch.cat(value, 0)
  167. new_batch[k] = value
  168. new_batch['batch_idx'] = list(new_batch['batch_idx'])
  169. for i in range(len(new_batch['batch_idx'])):
  170. new_batch['batch_idx'][i] += i # add target image index for build_targets()
  171. new_batch['batch_idx'] = torch.cat(new_batch['batch_idx'], 0)
  172. return new_batch
  173. # Classification dataloaders -------------------------------------------------------------------------------------------
  174. class ClassificationDataset(torchvision.datasets.ImageFolder):
  175. """
  176. YOLO Classification Dataset.
  177. Args:
  178. root (str): Dataset path.
  179. Attributes:
  180. cache_ram (bool): True if images should be cached in RAM, False otherwise.
  181. cache_disk (bool): True if images should be cached on disk, False otherwise.
  182. samples (list): List of samples containing file, index, npy, and im.
  183. torch_transforms (callable): torchvision transforms applied to the dataset.
  184. album_transforms (callable, optional): Albumentations transforms applied to the dataset if augment is True.
  185. """
  186. def __init__(self, root, args, augment=False, cache=False, prefix=''):
  187. """
  188. Initialize YOLO object with root, image size, augmentations, and cache settings.
  189. Args:
  190. root (str): Dataset path.
  191. args (Namespace): Argument parser containing dataset related settings.
  192. augment (bool, optional): True if dataset should be augmented, False otherwise. Defaults to False.
  193. cache (bool | str | optional): Cache setting, can be True, False, 'ram' or 'disk'. Defaults to False.
  194. """
  195. super().__init__(root=root)
  196. if augment and args.fraction < 1.0: # reduce training fraction
  197. self.samples = self.samples[:round(len(self.samples) * args.fraction)]
  198. self.prefix = colorstr(f'{prefix}: ') if prefix else ''
  199. self.cache_ram = cache is True or cache == 'ram'
  200. self.cache_disk = cache == 'disk'
  201. self.samples = self.verify_images() # filter out bad images
  202. self.samples = [list(x) + [Path(x[0]).with_suffix('.npy'), None] for x in self.samples] # file, index, npy, im
  203. self.torch_transforms = classify_transforms(args.imgsz)
  204. self.album_transforms = classify_albumentations(
  205. augment=augment,
  206. size=args.imgsz,
  207. scale=(1.0 - args.scale, 1.0), # (0.08, 1.0)
  208. hflip=args.fliplr,
  209. vflip=args.flipud,
  210. hsv_h=args.hsv_h, # HSV-Hue augmentation (fraction)
  211. hsv_s=args.hsv_s, # HSV-Saturation augmentation (fraction)
  212. hsv_v=args.hsv_v, # HSV-Value augmentation (fraction)
  213. mean=(0.0, 0.0, 0.0), # IMAGENET_MEAN
  214. std=(1.0, 1.0, 1.0), # IMAGENET_STD
  215. auto_aug=False) if augment else None
  216. def __getitem__(self, i):
  217. """Returns subset of data and targets corresponding to given indices."""
  218. f, j, fn, im = self.samples[i] # filename, index, filename.with_suffix('.npy'), image
  219. if self.cache_ram and im is None:
  220. im = self.samples[i][3] = cv2.imread(f)
  221. elif self.cache_disk:
  222. if not fn.exists(): # load npy
  223. np.save(fn.as_posix(), cv2.imread(f))
  224. im = np.load(fn)
  225. else: # read image
  226. im = cv2.imread(f) # BGR
  227. if self.album_transforms:
  228. sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))['image']
  229. else:
  230. sample = self.torch_transforms(im)
  231. return {'img': sample, 'cls': j}
  232. def __len__(self) -> int:
  233. return len(self.samples)
  234. def verify_images(self):
  235. """Verify all images in dataset."""
  236. desc = f'{self.prefix}Scanning {self.root}...'
  237. path = Path(self.root).with_suffix('.cache') # *.cache file path
  238. with contextlib.suppress(FileNotFoundError, AssertionError, AttributeError):
  239. cache = load_dataset_cache_file(path) # attempt to load a *.cache file
  240. assert cache['version'] == DATASET_CACHE_VERSION # matches current version
  241. assert cache['hash'] == get_hash([x[0] for x in self.samples]) # identical hash
  242. nf, nc, n, samples = cache.pop('results') # found, missing, empty, corrupt, total
  243. if LOCAL_RANK in (-1, 0):
  244. d = f'{desc} {nf} images, {nc} corrupt'
  245. tqdm(None, desc=d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT)
  246. if cache['msgs']:
  247. LOGGER.info('\n'.join(cache['msgs'])) # display warnings
  248. return samples
  249. # Run scan if *.cache retrieval failed
  250. nf, nc, msgs, samples, x = 0, 0, [], [], {}
  251. with ThreadPool(NUM_THREADS) as pool:
  252. results = pool.imap(func=verify_image, iterable=zip(self.samples, repeat(self.prefix)))
  253. pbar = tqdm(results, desc=desc, total=len(self.samples), bar_format=TQDM_BAR_FORMAT)
  254. for sample, nf_f, nc_f, msg in pbar:
  255. if nf_f:
  256. samples.append(sample)
  257. if msg:
  258. msgs.append(msg)
  259. nf += nf_f
  260. nc += nc_f
  261. pbar.desc = f'{desc} {nf} images, {nc} corrupt'
  262. pbar.close()
  263. if msgs:
  264. LOGGER.info('\n'.join(msgs))
  265. x['hash'] = get_hash([x[0] for x in self.samples])
  266. x['results'] = nf, nc, len(samples), samples
  267. x['msgs'] = msgs # warnings
  268. save_dataset_cache_file(self.prefix, path, x)
  269. return samples
  270. def load_dataset_cache_file(path):
  271. """Load an Ultralytics *.cache dictionary from path."""
  272. import gc
  273. gc.disable() # reduce pickle load time https://github.com/ultralytics/ultralytics/pull/1585
  274. cache = np.load(str(path), allow_pickle=True).item() # load dict
  275. gc.enable()
  276. return cache
  277. def save_dataset_cache_file(prefix, path, x):
  278. """Save an Ultralytics dataset *.cache dictionary x to path."""
  279. x['version'] = DATASET_CACHE_VERSION # add cache version
  280. if is_dir_writeable(path.parent):
  281. if path.exists():
  282. path.unlink() # remove *.cache file if exists
  283. np.save(str(path), x) # save cache for next time
  284. path.with_suffix('.cache.npy').rename(path) # remove .npy suffix
  285. LOGGER.info(f'{prefix}New cache created: {path}')
  286. else:
  287. LOGGER.warning(f'{prefix}WARNING ⚠️ Cache directory {path.parent} is not writeable, cache not saved.')
  288. # TODO: support semantic segmentation
  289. class SemanticDataset(BaseDataset):
  290. def __init__(self):
  291. """Initialize a SemanticDataset object."""
  292. super().__init__()