base.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. import glob
  3. import math
  4. import os
  5. import random
  6. from copy import deepcopy
  7. from multiprocessing.pool import ThreadPool
  8. from pathlib import Path
  9. from typing import Optional
  10. import cv2
  11. import numpy as np
  12. import psutil
  13. from torch.utils.data import Dataset
  14. from ultralytics.data.utils import FORMATS_HELP_MSG, HELP_URL, IMG_FORMATS
  15. from ultralytics.utils import DEFAULT_CFG, LOCAL_RANK, LOGGER, NUM_THREADS, TQDM
  16. class BaseDataset(Dataset):
  17. """
  18. Base dataset class for loading and processing image data.
  19. Args:
  20. img_path (str): Path to the folder containing images.
  21. imgsz (int, optional): Image size. Defaults to 640.
  22. cache (bool, optional): Cache images to RAM or disk during training. Defaults to False.
  23. augment (bool, optional): If True, data augmentation is applied. Defaults to True.
  24. hyp (dict, optional): Hyperparameters to apply data augmentation. Defaults to None.
  25. prefix (str, optional): Prefix to print in log messages. Defaults to ''.
  26. rect (bool, optional): If True, rectangular training is used. Defaults to False.
  27. batch_size (int, optional): Size of batches. Defaults to None.
  28. stride (int, optional): Stride. Defaults to 32.
  29. pad (float, optional): Padding. Defaults to 0.0.
  30. single_cls (bool, optional): If True, single class training is used. Defaults to False.
  31. classes (list): List of included classes. Default is None.
  32. fraction (float): Fraction of dataset to utilize. Default is 1.0 (use all data).
  33. Attributes:
  34. im_files (list): List of image file paths.
  35. labels (list): List of label data dictionaries.
  36. ni (int): Number of images in the dataset.
  37. ims (list): List of loaded images.
  38. npy_files (list): List of numpy file paths.
  39. transforms (callable): Image transformation function.
  40. """
  41. def __init__(
  42. self,
  43. img_path,
  44. imgsz=640,
  45. cache=False,
  46. augment=True,
  47. hyp=DEFAULT_CFG,
  48. prefix="",
  49. rect=False,
  50. batch_size=16,
  51. stride=32,
  52. pad=0.5,
  53. single_cls=False,
  54. classes=None,
  55. fraction=1.0,
  56. ):
  57. """Initialize BaseDataset with given configuration and options."""
  58. super().__init__()
  59. self.img_path = img_path
  60. self.imgsz = imgsz
  61. self.augment = augment
  62. self.single_cls = single_cls
  63. self.prefix = prefix
  64. self.fraction = fraction
  65. self.im_files = self.get_img_files(self.img_path)
  66. self.labels = self.get_labels()
  67. self.update_labels(include_class=classes) # single_cls and include_class
  68. self.ni = len(self.labels) # number of images
  69. self.rect = rect
  70. self.batch_size = batch_size
  71. self.stride = stride
  72. self.pad = pad
  73. if self.rect:
  74. assert self.batch_size is not None
  75. self.set_rectangle()
  76. # Buffer thread for mosaic images
  77. self.buffer = [] # buffer size = batch size
  78. self.max_buffer_length = min((self.ni, self.batch_size * 8, 1000)) if self.augment else 0
  79. # Cache images (options are cache = True, False, None, "ram", "disk")
  80. self.ims, self.im_hw0, self.im_hw = [None] * self.ni, [None] * self.ni, [None] * self.ni
  81. self.npy_files = [Path(f).with_suffix(".npy") for f in self.im_files]
  82. self.cache = cache.lower() if isinstance(cache, str) else "ram" if cache is True else None
  83. if self.cache == "ram" and self.check_cache_ram():
  84. if hyp.deterministic:
  85. LOGGER.warning(
  86. "WARNING ⚠️ cache='ram' may produce non-deterministic training results. "
  87. "Consider cache='disk' as a deterministic alternative if your disk space allows."
  88. )
  89. self.cache_images()
  90. elif self.cache == "disk" and self.check_cache_disk():
  91. self.cache_images()
  92. # Transforms
  93. self.transforms = self.build_transforms(hyp=hyp)
  94. def get_img_files(self, img_path):
  95. """Read image files."""
  96. try:
  97. f = [] # image files
  98. for p in img_path if isinstance(img_path, list) else [img_path]:
  99. p = Path(p) # os-agnostic
  100. if p.is_dir(): # dir
  101. f += glob.glob(str(p / "**" / "*.*"), recursive=True)
  102. # F = list(p.rglob('*.*')) # pathlib
  103. elif p.is_file(): # file
  104. with open(p) as t:
  105. t = t.read().strip().splitlines()
  106. parent = str(p.parent) + os.sep
  107. f += [x.replace("./", parent) if x.startswith("./") else x for x in t] # local to global path
  108. # F += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
  109. else:
  110. raise FileNotFoundError(f"{self.prefix}{p} does not exist")
  111. im_files = sorted(x.replace("/", os.sep) for x in f if x.split(".")[-1].lower() in IMG_FORMATS)
  112. # self.img_files = sorted([x for x in f if x.suffix[1:].lower() in IMG_FORMATS]) # pathlib
  113. assert im_files, f"{self.prefix}No images found in {img_path}. {FORMATS_HELP_MSG}"
  114. except Exception as e:
  115. raise FileNotFoundError(f"{self.prefix}Error loading data from {img_path}\n{HELP_URL}") from e
  116. if self.fraction < 1:
  117. im_files = im_files[: round(len(im_files) * self.fraction)] # retain a fraction of the dataset
  118. return im_files
  119. def update_labels(self, include_class: Optional[list]):
  120. """Update labels to include only these classes (optional)."""
  121. include_class_array = np.array(include_class).reshape(1, -1)
  122. for i in range(len(self.labels)):
  123. if include_class is not None:
  124. cls = self.labels[i]["cls"]
  125. bboxes = self.labels[i]["bboxes"]
  126. segments = self.labels[i]["segments"]
  127. keypoints = self.labels[i]["keypoints"]
  128. j = (cls == include_class_array).any(1)
  129. self.labels[i]["cls"] = cls[j]
  130. self.labels[i]["bboxes"] = bboxes[j]
  131. if segments:
  132. self.labels[i]["segments"] = [segments[si] for si, idx in enumerate(j) if idx]
  133. if keypoints is not None:
  134. self.labels[i]["keypoints"] = keypoints[j]
  135. if self.single_cls:
  136. self.labels[i]["cls"][:, 0] = 0
  137. def load_image(self, i, rect_mode=True):
  138. """Loads 1 image from dataset index 'i', returns (im, resized hw)."""
  139. im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i]
  140. if im is None: # not cached in RAM
  141. if fn.exists(): # load npy
  142. try:
  143. im = np.load(fn)
  144. except Exception as e:
  145. LOGGER.warning(f"{self.prefix}WARNING ⚠️ Removing corrupt *.npy image file {fn} due to: {e}")
  146. Path(fn).unlink(missing_ok=True)
  147. im = cv2.imread(f) # BGR
  148. else: # read image
  149. im = cv2.imread(f) # BGR
  150. if im is None:
  151. raise FileNotFoundError(f"Image Not Found {f}")
  152. h0, w0 = im.shape[:2] # orig hw
  153. if rect_mode: # resize long side to imgsz while maintaining aspect ratio
  154. r = self.imgsz / max(h0, w0) # ratio
  155. if r != 1: # if sizes are not equal
  156. w, h = (min(math.ceil(w0 * r), self.imgsz), min(math.ceil(h0 * r), self.imgsz))
  157. im = cv2.resize(im, (w, h), interpolation=cv2.INTER_LINEAR)
  158. elif not (h0 == w0 == self.imgsz): # resize by stretching image to square imgsz
  159. im = cv2.resize(im, (self.imgsz, self.imgsz), interpolation=cv2.INTER_LINEAR)
  160. # Add to buffer if training with augmentations
  161. if self.augment:
  162. self.ims[i], self.im_hw0[i], self.im_hw[i] = im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized
  163. self.buffer.append(i)
  164. if 1 < len(self.buffer) >= self.max_buffer_length: # prevent empty buffer
  165. j = self.buffer.pop(0)
  166. if self.cache != "ram":
  167. self.ims[j], self.im_hw0[j], self.im_hw[j] = None, None, None
  168. return im, (h0, w0), im.shape[:2]
  169. return self.ims[i], self.im_hw0[i], self.im_hw[i]
  170. def cache_images(self):
  171. """Cache images to memory or disk."""
  172. b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
  173. fcn, storage = (self.cache_images_to_disk, "Disk") if self.cache == "disk" else (self.load_image, "RAM")
  174. with ThreadPool(NUM_THREADS) as pool:
  175. results = pool.imap(fcn, range(self.ni))
  176. pbar = TQDM(enumerate(results), total=self.ni, disable=LOCAL_RANK > 0)
  177. for i, x in pbar:
  178. if self.cache == "disk":
  179. b += self.npy_files[i].stat().st_size
  180. else: # 'ram'
  181. self.ims[i], self.im_hw0[i], self.im_hw[i] = x # im, hw_orig, hw_resized = load_image(self, i)
  182. b += self.ims[i].nbytes
  183. pbar.desc = f"{self.prefix}Caching images ({b / gb:.1f}GB {storage})"
  184. pbar.close()
  185. def cache_images_to_disk(self, i):
  186. """Saves an image as an *.npy file for faster loading."""
  187. f = self.npy_files[i]
  188. if not f.exists():
  189. np.save(f.as_posix(), cv2.imread(self.im_files[i]), allow_pickle=False)
  190. def check_cache_disk(self, safety_margin=0.5):
  191. """Check image caching requirements vs available disk space."""
  192. import shutil
  193. b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
  194. n = min(self.ni, 30) # extrapolate from 30 random images
  195. for _ in range(n):
  196. im_file = random.choice(self.im_files)
  197. im = cv2.imread(im_file)
  198. if im is None:
  199. continue
  200. b += im.nbytes
  201. if not os.access(Path(im_file).parent, os.W_OK):
  202. self.cache = None
  203. LOGGER.info(f"{self.prefix}Skipping caching images to disk, directory not writeable ⚠️")
  204. return False
  205. disk_required = b * self.ni / n * (1 + safety_margin) # bytes required to cache dataset to disk
  206. total, used, free = shutil.disk_usage(Path(self.im_files[0]).parent)
  207. if disk_required > free:
  208. self.cache = None
  209. LOGGER.info(
  210. f"{self.prefix}{disk_required / gb:.1f}GB disk space required, "
  211. f"with {int(safety_margin * 100)}% safety margin but only "
  212. f"{free / gb:.1f}/{total / gb:.1f}GB free, not caching images to disk ⚠️"
  213. )
  214. return False
  215. return True
  216. def check_cache_ram(self, safety_margin=0.5):
  217. """Check image caching requirements vs available memory."""
  218. b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes
  219. n = min(self.ni, 30) # extrapolate from 30 random images
  220. for _ in range(n):
  221. im = cv2.imread(random.choice(self.im_files)) # sample image
  222. if im is None:
  223. continue
  224. ratio = self.imgsz / max(im.shape[0], im.shape[1]) # max(h, w) # ratio
  225. b += im.nbytes * ratio**2
  226. mem_required = b * self.ni / n * (1 + safety_margin) # GB required to cache dataset into RAM
  227. mem = psutil.virtual_memory()
  228. if mem_required > mem.available:
  229. self.cache = None
  230. LOGGER.info(
  231. f"{self.prefix}{mem_required / gb:.1f}GB RAM required to cache images "
  232. f"with {int(safety_margin * 100)}% safety margin but only "
  233. f"{mem.available / gb:.1f}/{mem.total / gb:.1f}GB available, not caching images ⚠️"
  234. )
  235. return False
  236. return True
  237. def set_rectangle(self):
  238. """Sets the shape of bounding boxes for YOLO detections as rectangles."""
  239. bi = np.floor(np.arange(self.ni) / self.batch_size).astype(int) # batch index
  240. nb = bi[-1] + 1 # number of batches
  241. s = np.array([x.pop("shape") for x in self.labels]) # hw
  242. ar = s[:, 0] / s[:, 1] # aspect ratio
  243. irect = ar.argsort()
  244. self.im_files = [self.im_files[i] for i in irect]
  245. self.labels = [self.labels[i] for i in irect]
  246. ar = ar[irect]
  247. # Set training image shapes
  248. shapes = [[1, 1]] * nb
  249. for i in range(nb):
  250. ari = ar[bi == i]
  251. mini, maxi = ari.min(), ari.max()
  252. if maxi < 1:
  253. shapes[i] = [maxi, 1]
  254. elif mini > 1:
  255. shapes[i] = [1, 1 / mini]
  256. self.batch_shapes = np.ceil(np.array(shapes) * self.imgsz / self.stride + self.pad).astype(int) * self.stride
  257. self.batch = bi # batch index of image
  258. def __getitem__(self, index):
  259. """Returns transformed label information for given index."""
  260. return self.transforms(self.get_image_and_label(index))
  261. def get_image_and_label(self, index):
  262. """Get and return label information from the dataset."""
  263. label = deepcopy(self.labels[index]) # requires deepcopy() https://github.com/ultralytics/ultralytics/pull/1948
  264. label.pop("shape", None) # shape is for rect, remove it
  265. label["img"], label["ori_shape"], label["resized_shape"] = self.load_image(index)
  266. label["ratio_pad"] = (
  267. label["resized_shape"][0] / label["ori_shape"][0],
  268. label["resized_shape"][1] / label["ori_shape"][1],
  269. ) # for evaluation
  270. if self.rect:
  271. label["rect_shape"] = self.batch_shapes[self.batch[index]]
  272. return self.update_labels_info(label)
  273. def __len__(self):
  274. """Returns the length of the labels list for the dataset."""
  275. return len(self.labels)
  276. def update_labels_info(self, label):
  277. """Custom your label format here."""
  278. return label
  279. def build_transforms(self, hyp=None):
  280. """
  281. Users can customize augmentations here.
  282. Example:
  283. ```python
  284. if self.augment:
  285. # Training transforms
  286. return Compose([])
  287. else:
  288. # Val transforms
  289. return Compose([])
  290. ```
  291. """
  292. raise NotImplementedError
  293. def get_labels(self):
  294. """
  295. Users can customize their own format here.
  296. Note:
  297. Ensure output is a dictionary with the following keys:
  298. ```python
  299. dict(
  300. im_file=im_file,
  301. shape=shape, # format: (height, width)
  302. cls=cls,
  303. bboxes=bboxes, # xywh
  304. segments=segments, # xy
  305. keypoints=keypoints, # xy
  306. normalized=True, # or False
  307. bbox_format="xyxy", # or xywh, ltwh
  308. )
  309. ```
  310. """
  311. raise NotImplementedError