segment-everything 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. segment_everything/__init__.py +5 -0
  2. segment_everything/augmentation/albumentations_helper.py +0 -0
  3. segment_everything/detect_and_segment.py +131 -0
  4. segment_everything/napari_helper.py +15 -0
  5. segment_everything/prompt_generator.py +188 -0
  6. segment_everything/py.typed +5 -0
  7. segment_everything/stacked_label_dataset.py +113 -0
  8. segment_everything/stacked_labels.py +428 -0
  9. segment_everything/vendored/PromptGuidedDecoder/Prompt_guided_Mask_Decoder.pt +0 -0
  10. segment_everything/vendored/__init__.py +5 -0
  11. segment_everything/vendored/dice.py +158 -0
  12. segment_everything/vendored/efficientvit/__init__.py +0 -0
  13. segment_everything/vendored/efficientvit/apps/__init__.py +0 -0
  14. segment_everything/vendored/efficientvit/apps/data_provider/__init__.py +7 -0
  15. segment_everything/vendored/efficientvit/apps/data_provider/augment/__init__.py +6 -0
  16. segment_everything/vendored/efficientvit/apps/data_provider/augment/bbox.py +30 -0
  17. segment_everything/vendored/efficientvit/apps/data_provider/augment/color_aug.py +78 -0
  18. segment_everything/vendored/efficientvit/apps/data_provider/base.py +254 -0
  19. segment_everything/vendored/efficientvit/apps/data_provider/random_resolution/__init__.py +6 -0
  20. segment_everything/vendored/efficientvit/apps/data_provider/random_resolution/_data_loader.py +1538 -0
  21. segment_everything/vendored/efficientvit/apps/data_provider/random_resolution/_data_worker.py +357 -0
  22. segment_everything/vendored/efficientvit/apps/data_provider/random_resolution/controller.py +100 -0
  23. segment_everything/vendored/efficientvit/apps/setup.py +150 -0
  24. segment_everything/vendored/efficientvit/apps/trainer/__init__.py +6 -0
  25. segment_everything/vendored/efficientvit/apps/trainer/base.py +318 -0
  26. segment_everything/vendored/efficientvit/apps/trainer/run_config.py +129 -0
  27. segment_everything/vendored/efficientvit/apps/utils/__init__.py +12 -0
  28. segment_everything/vendored/efficientvit/apps/utils/dist.py +32 -0
  29. segment_everything/vendored/efficientvit/apps/utils/ema.py +52 -0
  30. segment_everything/vendored/efficientvit/apps/utils/export.py +45 -0
  31. segment_everything/vendored/efficientvit/apps/utils/init.py +66 -0
  32. segment_everything/vendored/efficientvit/apps/utils/lr.py +52 -0
  33. segment_everything/vendored/efficientvit/apps/utils/metric.py +43 -0
  34. segment_everything/vendored/efficientvit/apps/utils/misc.py +101 -0
  35. segment_everything/vendored/efficientvit/apps/utils/opt.py +28 -0
  36. segment_everything/vendored/efficientvit/cls_model_zoo.py +79 -0
  37. segment_everything/vendored/efficientvit/clscore/__init__.py +0 -0
  38. segment_everything/vendored/efficientvit/clscore/data_provider/__init__.py +5 -0
  39. segment_everything/vendored/efficientvit/clscore/data_provider/imagenet.py +142 -0
  40. segment_everything/vendored/efficientvit/clscore/trainer/__init__.py +6 -0
  41. segment_everything/vendored/efficientvit/clscore/trainer/cls_run_config.py +18 -0
  42. segment_everything/vendored/efficientvit/clscore/trainer/cls_trainer.py +265 -0
  43. segment_everything/vendored/efficientvit/clscore/trainer/utils/__init__.py +7 -0
  44. segment_everything/vendored/efficientvit/clscore/trainer/utils/label_smooth.py +18 -0
  45. segment_everything/vendored/efficientvit/clscore/trainer/utils/metric.py +23 -0
  46. segment_everything/vendored/efficientvit/clscore/trainer/utils/mixup.py +67 -0
  47. segment_everything/vendored/efficientvit/models/__init__.py +0 -0
  48. segment_everything/vendored/efficientvit/models/efficientvit/__init__.py +8 -0
  49. segment_everything/vendored/efficientvit/models/efficientvit/backbone.py +380 -0
  50. segment_everything/vendored/efficientvit/models/efficientvit/cls.py +188 -0
  51. segment_everything/vendored/efficientvit/models/efficientvit/sam.py +181 -0
  52. segment_everything/vendored/efficientvit/models/efficientvit/seg.py +373 -0
  53. segment_everything/vendored/efficientvit/models/nn/__init__.py +8 -0
  54. segment_everything/vendored/efficientvit/models/nn/act.py +30 -0
  55. segment_everything/vendored/efficientvit/models/nn/drop.py +104 -0
  56. segment_everything/vendored/efficientvit/models/nn/norm.py +164 -0
  57. segment_everything/vendored/efficientvit/models/nn/ops.py +597 -0
  58. segment_everything/vendored/efficientvit/models/utils/__init__.py +7 -0
  59. segment_everything/vendored/efficientvit/models/utils/list.py +53 -0
  60. segment_everything/vendored/efficientvit/models/utils/network.py +73 -0
  61. segment_everything/vendored/efficientvit/models/utils/random.py +65 -0
  62. segment_everything/vendored/efficientvit/sam_model_zoo.py +45 -0
  63. segment_everything/vendored/efficientvit/seg_model_zoo.py +70 -0
  64. segment_everything/vendored/get_object_aware.py +26 -0
  65. segment_everything/vendored/mobilesamv2/__init__.py +16 -0
  66. segment_everything/vendored/mobilesamv2/automatic_mask_generator.py +415 -0
  67. segment_everything/vendored/mobilesamv2/build_sam.py +246 -0
  68. segment_everything/vendored/mobilesamv2/modeling/__init__.py +11 -0
  69. segment_everything/vendored/mobilesamv2/modeling/common.py +43 -0
  70. segment_everything/vendored/mobilesamv2/modeling/image_encoder.py +394 -0
  71. segment_everything/vendored/mobilesamv2/modeling/mask_decoder.py +213 -0
  72. segment_everything/vendored/mobilesamv2/modeling/prompt_encoder.py +217 -0
  73. segment_everything/vendored/mobilesamv2/modeling/sam.py +203 -0
  74. segment_everything/vendored/mobilesamv2/modeling/transformer.py +240 -0
  75. segment_everything/vendored/mobilesamv2/predictor.py +384 -0
  76. segment_everything/vendored/mobilesamv2/utils/__init__.py +5 -0
  77. segment_everything/vendored/mobilesamv2/utils/amg.py +347 -0
  78. segment_everything/vendored/mobilesamv2/utils/onnx.py +144 -0
  79. segment_everything/vendored/mobilesamv2/utils/transforms.py +103 -0
  80. segment_everything/vendored/object_detection/__init__.py +0 -0
  81. segment_everything/vendored/object_detection/ultralytics/__init__.py +5 -0
  82. segment_everything/vendored/object_detection/ultralytics/nn/__init__.py +9 -0
  83. segment_everything/vendored/object_detection/ultralytics/nn/autobackend.py +658 -0
  84. segment_everything/vendored/object_detection/ultralytics/nn/autoshape.py +397 -0
  85. segment_everything/vendored/object_detection/ultralytics/nn/modules/__init__.py +110 -0
  86. segment_everything/vendored/object_detection/ultralytics/nn/modules/block.py +304 -0
  87. segment_everything/vendored/object_detection/ultralytics/nn/modules/conv.py +297 -0
  88. segment_everything/vendored/object_detection/ultralytics/nn/modules/head.py +468 -0
  89. segment_everything/vendored/object_detection/ultralytics/nn/modules/transformer.py +378 -0
  90. segment_everything/vendored/object_detection/ultralytics/nn/modules/utils.py +78 -0
  91. segment_everything/vendored/object_detection/ultralytics/nn/tasks.py +1049 -0
  92. segment_everything/vendored/object_detection/ultralytics/prompt_mobilesamv2/__init__.py +6 -0
  93. segment_everything/vendored/object_detection/ultralytics/prompt_mobilesamv2/model.py +104 -0
  94. segment_everything/vendored/object_detection/ultralytics/prompt_mobilesamv2/predict.py +95 -0
  95. segment_everything/vendored/object_detection/ultralytics/yolo/__init__.py +5 -0
  96. segment_everything/vendored/object_detection/ultralytics/yolo/cfg/__init__.py +588 -0
  97. segment_everything/vendored/object_detection/ultralytics/yolo/cfg/default.yaml +117 -0
  98. segment_everything/vendored/object_detection/ultralytics/yolo/data/__init__.py +9 -0
  99. segment_everything/vendored/object_detection/ultralytics/yolo/data/annotator.py +53 -0
  100. segment_everything/vendored/object_detection/ultralytics/yolo/data/augment.py +899 -0
  101. segment_everything/vendored/object_detection/ultralytics/yolo/data/base.py +286 -0
  102. segment_everything/vendored/object_detection/ultralytics/yolo/data/build.py +213 -0
  103. segment_everything/vendored/object_detection/ultralytics/yolo/data/converter.py +358 -0
  104. segment_everything/vendored/object_detection/ultralytics/yolo/data/dataloaders/__init__.py +0 -0
  105. segment_everything/vendored/object_detection/ultralytics/yolo/data/dataloaders/stream_loaders.py +459 -0
  106. segment_everything/vendored/object_detection/ultralytics/yolo/data/dataset.py +274 -0
  107. segment_everything/vendored/object_detection/ultralytics/yolo/data/dataset_wrappers.py +53 -0
  108. segment_everything/vendored/object_detection/ultralytics/yolo/data/scripts/download_weights.sh +18 -0
  109. segment_everything/vendored/object_detection/ultralytics/yolo/data/scripts/get_coco.sh +60 -0
  110. segment_everything/vendored/object_detection/ultralytics/yolo/data/scripts/get_coco128.sh +17 -0
  111. segment_everything/vendored/object_detection/ultralytics/yolo/data/scripts/get_imagenet.sh +51 -0
  112. segment_everything/vendored/object_detection/ultralytics/yolo/data/utils.py +716 -0
  113. segment_everything/vendored/object_detection/ultralytics/yolo/engine/__init__.py +0 -0
  114. segment_everything/vendored/object_detection/ultralytics/yolo/engine/exporter.py +1214 -0
  115. segment_everything/vendored/object_detection/ultralytics/yolo/engine/model.py +641 -0
  116. segment_everything/vendored/object_detection/ultralytics/yolo/engine/predictor.py +461 -0
  117. segment_everything/vendored/object_detection/ultralytics/yolo/engine/results.py +741 -0
  118. segment_everything/vendored/object_detection/ultralytics/yolo/utils/__init__.py +893 -0
  119. segment_everything/vendored/object_detection/ultralytics/yolo/utils/autobatch.py +108 -0
  120. segment_everything/vendored/object_detection/ultralytics/yolo/utils/callbacks/__init__.py +5 -0
  121. segment_everything/vendored/object_detection/ultralytics/yolo/utils/callbacks/base.py +212 -0
  122. segment_everything/vendored/object_detection/ultralytics/yolo/utils/checks.py +547 -0
  123. segment_everything/vendored/object_detection/ultralytics/yolo/utils/dist.py +67 -0
  124. segment_everything/vendored/object_detection/ultralytics/yolo/utils/downloads.py +353 -0
  125. segment_everything/vendored/object_detection/ultralytics/yolo/utils/errors.py +12 -0
  126. segment_everything/vendored/object_detection/ultralytics/yolo/utils/files.py +100 -0
  127. segment_everything/vendored/object_detection/ultralytics/yolo/utils/instance.py +391 -0
  128. segment_everything/vendored/object_detection/ultralytics/yolo/utils/loss.py +579 -0
  129. segment_everything/vendored/object_detection/ultralytics/yolo/utils/metrics.py +1189 -0
  130. segment_everything/vendored/object_detection/ultralytics/yolo/utils/ops.py +870 -0
  131. segment_everything/vendored/object_detection/ultralytics/yolo/utils/patches.py +45 -0
  132. segment_everything/vendored/object_detection/ultralytics/yolo/utils/plotting.py +767 -0
  133. segment_everything/vendored/object_detection/ultralytics/yolo/utils/tal.py +276 -0
  134. segment_everything/vendored/object_detection/ultralytics/yolo/utils/torch_utils.py +684 -0
  135. segment_everything/vendored/object_detection/ultralytics/yolo/utils/tuner.py +54 -0
  136. segment_everything/vendored/object_detection/ultralytics/yolo/v8/__init__.py +5 -0
  137. segment_everything/vendored/object_detection/ultralytics/yolo/v8/detect/__init__.py +5 -0
  138. segment_everything/vendored/object_detection/ultralytics/yolo/v8/detect/predict.py +69 -0
  139. segment_everything/vendored/tinyvit/__init__.py +2 -0
  140. segment_everything/vendored/tinyvit/tiny_vit.py +867 -0
  141. segment_everything/weights_helper.py +124 -0
  142. segment_everything-0.1.0.dist-info/METADATA +53 -0
  143. segment_everything-0.1.0.dist-info/RECORD +145 -0
  144. segment_everything-0.1.0.dist-info/WHEEL +4 -0
  145. segment_everything-0.1.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,899 @@
1
+ # Ultralytics YOLO 🚀, AGPL-3.0 license
2
+
3
+ import math
4
+ import random
5
+ from copy import deepcopy
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import torch
10
+ import torchvision.transforms as T
11
+
12
+ from ..utils import LOGGER, colorstr
13
+ from ..utils.checks import check_version
14
+ from ..utils.instance import Instances
15
+ from ..utils.metrics import bbox_ioa
16
+ from ..utils.ops import segment2box
17
+ from .utils import polygons2masks, polygons2masks_overlap
18
+
19
+ POSE_FLIPLR_INDEX = [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]
20
+
21
+
22
+ # TODO: we might need a BaseTransform to make all these augments be compatible with both classification and semantic
23
+ class BaseTransform:
24
+
25
+ def __init__(self) -> None:
26
+ pass
27
+
28
+ def apply_image(self, labels):
29
+ """Applies image transformation to labels."""
30
+ pass
31
+
32
+ def apply_instances(self, labels):
33
+ """Applies transformations to input 'labels' and returns object instances."""
34
+ pass
35
+
36
+ def apply_semantic(self, labels):
37
+ """Applies semantic segmentation to an image."""
38
+ pass
39
+
40
+ def __call__(self, labels):
41
+ """Applies label transformations to an image, instances and semantic masks."""
42
+ self.apply_image(labels)
43
+ self.apply_instances(labels)
44
+ self.apply_semantic(labels)
45
+
46
+
47
+ class Compose:
48
+
49
+ def __init__(self, transforms):
50
+ """Initializes the Compose object with a list of transforms."""
51
+ self.transforms = transforms
52
+
53
+ def __call__(self, data):
54
+ """Applies a series of transformations to input data."""
55
+ for t in self.transforms:
56
+ data = t(data)
57
+ return data
58
+
59
+ def append(self, transform):
60
+ """Appends a new transform to the existing list of transforms."""
61
+ self.transforms.append(transform)
62
+
63
+ def tolist(self):
64
+ """Converts list of transforms to a standard Python list."""
65
+ return self.transforms
66
+
67
+ def __repr__(self):
68
+ """Return string representation of object."""
69
+ format_string = f'{self.__class__.__name__}('
70
+ for t in self.transforms:
71
+ format_string += '\n'
72
+ format_string += f' {t}'
73
+ format_string += '\n)'
74
+ return format_string
75
+
76
+
77
+ class BaseMixTransform:
78
+ """This implementation is from mmyolo."""
79
+
80
+ def __init__(self, dataset, pre_transform=None, p=0.0) -> None:
81
+ self.dataset = dataset
82
+ self.pre_transform = pre_transform
83
+ self.p = p
84
+
85
+ def __call__(self, labels):
86
+ """Applies pre-processing transforms and mixup/mosaic transforms to labels data."""
87
+ if random.uniform(0, 1) > self.p:
88
+ return labels
89
+
90
+ # Get index of one or three other images
91
+ indexes = self.get_indexes()
92
+ if isinstance(indexes, int):
93
+ indexes = [indexes]
94
+
95
+ # Get images information will be used for Mosaic or MixUp
96
+ mix_labels = [self.dataset.get_image_and_label(i) for i in indexes]
97
+
98
+ if self.pre_transform is not None:
99
+ for i, data in enumerate(mix_labels):
100
+ mix_labels[i] = self.pre_transform(data)
101
+ labels['mix_labels'] = mix_labels
102
+
103
+ # Mosaic or MixUp
104
+ labels = self._mix_transform(labels)
105
+ labels.pop('mix_labels', None)
106
+ return labels
107
+
108
+ def _mix_transform(self, labels):
109
+ """Applies MixUp or Mosaic augmentation to the label dictionary."""
110
+ raise NotImplementedError
111
+
112
+ def get_indexes(self):
113
+ """Gets a list of shuffled indexes for mosaic augmentation."""
114
+ raise NotImplementedError
115
+
116
+
117
+ class Mosaic(BaseMixTransform):
118
+ """
119
+ Mosaic augmentation.
120
+
121
+ This class performs mosaic augmentation by combining multiple (4 or 9) images into a single mosaic image.
122
+ The augmentation is applied to a dataset with a given probability.
123
+
124
+ Attributes:
125
+ dataset: The dataset on which the mosaic augmentation is applied.
126
+ imgsz (int, optional): Image size (height and width) after mosaic pipeline of a single image. Default to 640.
127
+ p (float, optional): Probability of applying the mosaic augmentation. Must be in the range 0-1. Default to 1.0.
128
+ n (int, optional): The grid size, either 4 (for 2x2) or 9 (for 3x3).
129
+ """
130
+
131
+ def __init__(self, dataset, imgsz=640, p=1.0, n=4):
132
+ """Initializes the object with a dataset, image size, probability, and border."""
133
+ assert 0 <= p <= 1.0, f'The probability should be in range [0, 1], but got {p}.'
134
+ assert n in (4, 9), 'grid must be equal to 4 or 9.'
135
+ super().__init__(dataset=dataset, p=p)
136
+ self.dataset = dataset
137
+ self.imgsz = imgsz
138
+ self.border = (-imgsz // 2, -imgsz // 2) # width, height
139
+ self.n = n
140
+
141
+ def get_indexes(self, buffer=True):
142
+ """Return a list of random indexes from the dataset."""
143
+ if buffer: # select images from buffer
144
+ return random.choices(list(self.dataset.buffer), k=self.n - 1)
145
+ else: # select any images
146
+ return [random.randint(0, len(self.dataset) - 1) for _ in range(self.n - 1)]
147
+
148
+ def _mix_transform(self, labels):
149
+ """Apply mixup transformation to the input image and labels."""
150
+ assert labels.get('rect_shape', None) is None, 'rect and mosaic are mutually exclusive.'
151
+ assert len(labels.get('mix_labels', [])), 'There are no other images for mosaic augment.'
152
+ return self._mosaic4(labels) if self.n == 4 else self._mosaic9(labels)
153
+
154
+ def _mosaic4(self, labels):
155
+ """Create a 2x2 image mosaic."""
156
+ mosaic_labels = []
157
+ s = self.imgsz
158
+ yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.border) # mosaic center x, y
159
+ for i in range(4):
160
+ labels_patch = labels if i == 0 else labels['mix_labels'][i - 1]
161
+ # Load image
162
+ img = labels_patch['img']
163
+ h, w = labels_patch.pop('resized_shape')
164
+
165
+ # Place img in img4
166
+ if i == 0: # top left
167
+ img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
168
+ x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)
169
+ x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h # xmin, ymin, xmax, ymax (small image)
170
+ elif i == 1: # top right
171
+ x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc
172
+ x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h
173
+ elif i == 2: # bottom left
174
+ x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)
175
+ x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, w, min(y2a - y1a, h)
176
+ elif i == 3: # bottom right
177
+ x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)
178
+ x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)
179
+
180
+ img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] # img4[ymin:ymax, xmin:xmax]
181
+ padw = x1a - x1b
182
+ padh = y1a - y1b
183
+
184
+ labels_patch = self._update_labels(labels_patch, padw, padh)
185
+ mosaic_labels.append(labels_patch)
186
+ final_labels = self._cat_labels(mosaic_labels)
187
+ final_labels['img'] = img4
188
+ return final_labels
189
+
190
+ def _mosaic9(self, labels):
191
+ """Create a 3x3 image mosaic."""
192
+ mosaic_labels = []
193
+ s = self.imgsz
194
+ hp, wp = -1, -1 # height, width previous
195
+ for i in range(9):
196
+ labels_patch = labels if i == 0 else labels['mix_labels'][i - 1]
197
+ # Load image
198
+ img = labels_patch['img']
199
+ h, w = labels_patch.pop('resized_shape')
200
+
201
+ # Place img in img9
202
+ if i == 0: # center
203
+ img9 = np.full((s * 3, s * 3, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
204
+ h0, w0 = h, w
205
+ c = s, s, s + w, s + h # xmin, ymin, xmax, ymax (base) coordinates
206
+ elif i == 1: # top
207
+ c = s, s - h, s + w, s
208
+ elif i == 2: # top right
209
+ c = s + wp, s - h, s + wp + w, s
210
+ elif i == 3: # right
211
+ c = s + w0, s, s + w0 + w, s + h
212
+ elif i == 4: # bottom right
213
+ c = s + w0, s + hp, s + w0 + w, s + hp + h
214
+ elif i == 5: # bottom
215
+ c = s + w0 - w, s + h0, s + w0, s + h0 + h
216
+ elif i == 6: # bottom left
217
+ c = s + w0 - wp - w, s + h0, s + w0 - wp, s + h0 + h
218
+ elif i == 7: # left
219
+ c = s - w, s + h0 - h, s, s + h0
220
+ elif i == 8: # top left
221
+ c = s - w, s + h0 - hp - h, s, s + h0 - hp
222
+
223
+ padw, padh = c[:2]
224
+ x1, y1, x2, y2 = (max(x, 0) for x in c) # allocate coords
225
+
226
+ # Image
227
+ img9[y1:y2, x1:x2] = img[y1 - padh:, x1 - padw:] # img9[ymin:ymax, xmin:xmax]
228
+ hp, wp = h, w # height, width previous for next iteration
229
+
230
+ # Labels assuming imgsz*2 mosaic size
231
+ labels_patch = self._update_labels(labels_patch, padw + self.border[0], padh + self.border[1])
232
+ mosaic_labels.append(labels_patch)
233
+ final_labels = self._cat_labels(mosaic_labels)
234
+
235
+ final_labels['img'] = img9[-self.border[0]:self.border[0], -self.border[1]:self.border[1]]
236
+ return final_labels
237
+
238
+ @staticmethod
239
+ def _update_labels(labels, padw, padh):
240
+ """Update labels."""
241
+ nh, nw = labels['img'].shape[:2]
242
+ labels['instances'].convert_bbox(format='xyxy')
243
+ labels['instances'].denormalize(nw, nh)
244
+ labels['instances'].add_padding(padw, padh)
245
+ return labels
246
+
247
+ def _cat_labels(self, mosaic_labels):
248
+ """Return labels with mosaic border instances clipped."""
249
+ if len(mosaic_labels) == 0:
250
+ return {}
251
+ cls = []
252
+ instances = []
253
+ imgsz = self.imgsz * 2 # mosaic imgsz
254
+ for labels in mosaic_labels:
255
+ cls.append(labels['cls'])
256
+ instances.append(labels['instances'])
257
+ final_labels = {
258
+ 'im_file': mosaic_labels[0]['im_file'],
259
+ 'ori_shape': mosaic_labels[0]['ori_shape'],
260
+ 'resized_shape': (imgsz, imgsz),
261
+ 'cls': np.concatenate(cls, 0),
262
+ 'instances': Instances.concatenate(instances, axis=0),
263
+ 'mosaic_border': self.border} # final_labels
264
+ final_labels['instances'].clip(imgsz, imgsz)
265
+ good = final_labels['instances'].remove_zero_area_boxes()
266
+ final_labels['cls'] = final_labels['cls'][good]
267
+ return final_labels
268
+
269
+
270
+ class MixUp(BaseMixTransform):
271
+
272
+ def __init__(self, dataset, pre_transform=None, p=0.0) -> None:
273
+ super().__init__(dataset=dataset, pre_transform=pre_transform, p=p)
274
+
275
+ def get_indexes(self):
276
+ """Get a random index from the dataset."""
277
+ return random.randint(0, len(self.dataset) - 1)
278
+
279
+ def _mix_transform(self, labels):
280
+ """Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf."""
281
+ r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0
282
+ labels2 = labels['mix_labels'][0]
283
+ labels['img'] = (labels['img'] * r + labels2['img'] * (1 - r)).astype(np.uint8)
284
+ labels['instances'] = Instances.concatenate([labels['instances'], labels2['instances']], axis=0)
285
+ labels['cls'] = np.concatenate([labels['cls'], labels2['cls']], 0)
286
+ return labels
287
+
288
+
289
+ class RandomPerspective:
290
+
291
+ def __init__(self,
292
+ degrees=0.0,
293
+ translate=0.1,
294
+ scale=0.5,
295
+ shear=0.0,
296
+ perspective=0.0,
297
+ border=(0, 0),
298
+ pre_transform=None):
299
+ self.degrees = degrees
300
+ self.translate = translate
301
+ self.scale = scale
302
+ self.shear = shear
303
+ self.perspective = perspective
304
+ # Mosaic border
305
+ self.border = border
306
+ self.pre_transform = pre_transform
307
+
308
+ def affine_transform(self, img, border):
309
+ """Center."""
310
+ C = np.eye(3, dtype=np.float32)
311
+
312
+ C[0, 2] = -img.shape[1] / 2 # x translation (pixels)
313
+ C[1, 2] = -img.shape[0] / 2 # y translation (pixels)
314
+
315
+ # Perspective
316
+ P = np.eye(3, dtype=np.float32)
317
+ P[2, 0] = random.uniform(-self.perspective, self.perspective) # x perspective (about y)
318
+ P[2, 1] = random.uniform(-self.perspective, self.perspective) # y perspective (about x)
319
+
320
+ # Rotation and Scale
321
+ R = np.eye(3, dtype=np.float32)
322
+ a = random.uniform(-self.degrees, self.degrees)
323
+ # a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations
324
+ s = random.uniform(1 - self.scale, 1 + self.scale)
325
+ # s = 2 ** random.uniform(-scale, scale)
326
+ R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s)
327
+
328
+ # Shear
329
+ S = np.eye(3, dtype=np.float32)
330
+ S[0, 1] = math.tan(random.uniform(-self.shear, self.shear) * math.pi / 180) # x shear (deg)
331
+ S[1, 0] = math.tan(random.uniform(-self.shear, self.shear) * math.pi / 180) # y shear (deg)
332
+
333
+ # Translation
334
+ T = np.eye(3, dtype=np.float32)
335
+ T[0, 2] = random.uniform(0.5 - self.translate, 0.5 + self.translate) * self.size[0] # x translation (pixels)
336
+ T[1, 2] = random.uniform(0.5 - self.translate, 0.5 + self.translate) * self.size[1] # y translation (pixels)
337
+
338
+ # Combined rotation matrix
339
+ M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT
340
+ # Affine image
341
+ if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed
342
+ if self.perspective:
343
+ img = cv2.warpPerspective(img, M, dsize=self.size, borderValue=(114, 114, 114))
344
+ else: # affine
345
+ img = cv2.warpAffine(img, M[:2], dsize=self.size, borderValue=(114, 114, 114))
346
+ return img, M, s
347
+
348
+ def apply_bboxes(self, bboxes, M):
349
+ """
350
+ Apply affine to bboxes only.
351
+
352
+ Args:
353
+ bboxes (ndarray): list of bboxes, xyxy format, with shape (num_bboxes, 4).
354
+ M (ndarray): affine matrix.
355
+
356
+ Returns:
357
+ new_bboxes (ndarray): bboxes after affine, [num_bboxes, 4].
358
+ """
359
+ n = len(bboxes)
360
+ if n == 0:
361
+ return bboxes
362
+
363
+ xy = np.ones((n * 4, 3), dtype=bboxes.dtype)
364
+ xy[:, :2] = bboxes[:, [0, 1, 2, 3, 0, 3, 2, 1]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1
365
+ xy = xy @ M.T # transform
366
+ xy = (xy[:, :2] / xy[:, 2:3] if self.perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine
367
+
368
+ # Create new boxes
369
+ x = xy[:, [0, 2, 4, 6]]
370
+ y = xy[:, [1, 3, 5, 7]]
371
+ return np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1)), dtype=bboxes.dtype).reshape(4, n).T
372
+
373
+ def apply_segments(self, segments, M):
374
+ """
375
+ Apply affine to segments and generate new bboxes from segments.
376
+
377
+ Args:
378
+ segments (ndarray): list of segments, [num_samples, 500, 2].
379
+ M (ndarray): affine matrix.
380
+
381
+ Returns:
382
+ new_segments (ndarray): list of segments after affine, [num_samples, 500, 2].
383
+ new_bboxes (ndarray): bboxes after affine, [N, 4].
384
+ """
385
+ n, num = segments.shape[:2]
386
+ if n == 0:
387
+ return [], segments
388
+
389
+ xy = np.ones((n * num, 3), dtype=segments.dtype)
390
+ segments = segments.reshape(-1, 2)
391
+ xy[:, :2] = segments
392
+ xy = xy @ M.T # transform
393
+ xy = xy[:, :2] / xy[:, 2:3]
394
+ segments = xy.reshape(n, -1, 2)
395
+ bboxes = np.stack([segment2box(xy, self.size[0], self.size[1]) for xy in segments], 0)
396
+ return bboxes, segments
397
+
398
+ def apply_keypoints(self, keypoints, M):
399
+ """
400
+ Apply affine to keypoints.
401
+
402
+ Args:
403
+ keypoints (ndarray): keypoints, [N, 17, 3].
404
+ M (ndarray): affine matrix.
405
+
406
+ Return:
407
+ new_keypoints (ndarray): keypoints after affine, [N, 17, 3].
408
+ """
409
+ n, nkpt = keypoints.shape[:2]
410
+ if n == 0:
411
+ return keypoints
412
+ xy = np.ones((n * nkpt, 3), dtype=keypoints.dtype)
413
+ visible = keypoints[..., 2].reshape(n * nkpt, 1)
414
+ xy[:, :2] = keypoints[..., :2].reshape(n * nkpt, 2)
415
+ xy = xy @ M.T # transform
416
+ xy = xy[:, :2] / xy[:, 2:3] # perspective rescale or affine
417
+ out_mask = (xy[:, 0] < 0) | (xy[:, 1] < 0) | (xy[:, 0] > self.size[0]) | (xy[:, 1] > self.size[1])
418
+ visible[out_mask] = 0
419
+ return np.concatenate([xy, visible], axis=-1).reshape(n, nkpt, 3)
420
+
421
+ def __call__(self, labels):
422
+ """
423
+ Affine images and targets.
424
+
425
+ Args:
426
+ labels (dict): a dict of `bboxes`, `segments`, `keypoints`.
427
+ """
428
+ if self.pre_transform and 'mosaic_border' not in labels:
429
+ labels = self.pre_transform(labels)
430
+ labels.pop('ratio_pad') # do not need ratio pad
431
+
432
+ img = labels['img']
433
+ cls = labels['cls']
434
+ instances = labels.pop('instances')
435
+ # Make sure the coord formats are right
436
+ instances.convert_bbox(format='xyxy')
437
+ instances.denormalize(*img.shape[:2][::-1])
438
+
439
+ border = labels.pop('mosaic_border', self.border)
440
+ self.size = img.shape[1] + border[1] * 2, img.shape[0] + border[0] * 2 # w, h
441
+ # M is affine matrix
442
+ # scale for func:`box_candidates`
443
+ img, M, scale = self.affine_transform(img, border)
444
+
445
+ bboxes = self.apply_bboxes(instances.bboxes, M)
446
+
447
+ segments = instances.segments
448
+ keypoints = instances.keypoints
449
+ # Update bboxes if there are segments.
450
+ if len(segments):
451
+ bboxes, segments = self.apply_segments(segments, M)
452
+
453
+ if keypoints is not None:
454
+ keypoints = self.apply_keypoints(keypoints, M)
455
+ new_instances = Instances(bboxes, segments, keypoints, bbox_format='xyxy', normalized=False)
456
+ # Clip
457
+ new_instances.clip(*self.size)
458
+
459
+ # Filter instances
460
+ instances.scale(scale_w=scale, scale_h=scale, bbox_only=True)
461
+ # Make the bboxes have the same scale with new_bboxes
462
+ i = self.box_candidates(box1=instances.bboxes.T,
463
+ box2=new_instances.bboxes.T,
464
+ area_thr=0.01 if len(segments) else 0.10)
465
+ labels['instances'] = new_instances[i]
466
+ labels['cls'] = cls[i]
467
+ labels['img'] = img
468
+ labels['resized_shape'] = img.shape[:2]
469
+ return labels
470
+
471
+ def box_candidates(self, box1, box2, wh_thr=2, ar_thr=100, area_thr=0.1, eps=1e-16): # box1(4,n), box2(4,n)
472
+ # Compute box candidates: box1 before augment, box2 after augment, wh_thr (pixels), aspect_ratio_thr, area_ratio
473
+ w1, h1 = box1[2] - box1[0], box1[3] - box1[1]
474
+ w2, h2 = box2[2] - box2[0], box2[3] - box2[1]
475
+ ar = np.maximum(w2 / (h2 + eps), h2 / (w2 + eps)) # aspect ratio
476
+ return (w2 > wh_thr) & (h2 > wh_thr) & (w2 * h2 / (w1 * h1 + eps) > area_thr) & (ar < ar_thr) # candidates
477
+
478
+
479
+ class RandomHSV:
480
+
481
+ def __init__(self, hgain=0.5, sgain=0.5, vgain=0.5) -> None:
482
+ self.hgain = hgain
483
+ self.sgain = sgain
484
+ self.vgain = vgain
485
+
486
+ def __call__(self, labels):
487
+ """Applies random horizontal or vertical flip to an image with a given probability."""
488
+ img = labels['img']
489
+ if self.hgain or self.sgain or self.vgain:
490
+ r = np.random.uniform(-1, 1, 3) * [self.hgain, self.sgain, self.vgain] + 1 # random gains
491
+ hue, sat, val = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))
492
+ dtype = img.dtype # uint8
493
+
494
+ x = np.arange(0, 256, dtype=r.dtype)
495
+ lut_hue = ((x * r[0]) % 180).astype(dtype)
496
+ lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)
497
+ lut_val = np.clip(x * r[2], 0, 255).astype(dtype)
498
+
499
+ im_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))
500
+ cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR, dst=img) # no return needed
501
+ return labels
502
+
503
+
504
+ class RandomFlip:
505
+
506
+ def __init__(self, p=0.5, direction='horizontal', flip_idx=None) -> None:
507
+ assert direction in ['horizontal', 'vertical'], f'Support direction `horizontal` or `vertical`, got {direction}'
508
+ assert 0 <= p <= 1.0
509
+
510
+ self.p = p
511
+ self.direction = direction
512
+ self.flip_idx = flip_idx
513
+
514
+ def __call__(self, labels):
515
+ """Resize image and padding for detection, instance segmentation, pose."""
516
+ img = labels['img']
517
+ instances = labels.pop('instances')
518
+ instances.convert_bbox(format='xywh')
519
+ h, w = img.shape[:2]
520
+ h = 1 if instances.normalized else h
521
+ w = 1 if instances.normalized else w
522
+
523
+ # Flip up-down
524
+ if self.direction == 'vertical' and random.random() < self.p:
525
+ img = np.flipud(img)
526
+ instances.flipud(h)
527
+ if self.direction == 'horizontal' and random.random() < self.p:
528
+ img = np.fliplr(img)
529
+ instances.fliplr(w)
530
+ # For keypoints
531
+ if self.flip_idx is not None and instances.keypoints is not None:
532
+ instances.keypoints = np.ascontiguousarray(instances.keypoints[:, self.flip_idx, :])
533
+ labels['img'] = np.ascontiguousarray(img)
534
+ labels['instances'] = instances
535
+ return labels
536
+
537
+
538
+ class LetterBox:
539
+ """Resize image and padding for detection, instance segmentation, pose."""
540
+
541
+ def __init__(self, new_shape=(640, 640), auto=False, scaleFill=False, scaleup=True, stride=32):
542
+ """Initialize LetterBox object with specific parameters."""
543
+ self.new_shape = new_shape
544
+ self.auto = auto
545
+ self.scaleFill = scaleFill
546
+ self.scaleup = scaleup
547
+ self.stride = stride
548
+
549
+ def __call__(self, labels=None, image=None):
550
+ """Return updated labels and image with added border."""
551
+ if labels is None:
552
+ labels = {}
553
+ img = labels.get('img') if image is None else image
554
+ shape = img.shape[:2] # current shape [height, width]
555
+ new_shape = labels.pop('rect_shape', self.new_shape)
556
+ if isinstance(new_shape, int):
557
+ new_shape = (new_shape, new_shape)
558
+
559
+ # Scale ratio (new / old)
560
+ r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
561
+ if not self.scaleup: # only scale down, do not scale up (for better val mAP)
562
+ r = min(r, 1.0)
563
+
564
+ # Compute padding
565
+ ratio = r, r # width, height ratios
566
+ new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
567
+ dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
568
+ if self.auto: # minimum rectangle
569
+ dw, dh = np.mod(dw, self.stride), np.mod(dh, self.stride) # wh padding
570
+ elif self.scaleFill: # stretch
571
+ dw, dh = 0.0, 0.0
572
+ new_unpad = (new_shape[1], new_shape[0])
573
+ ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios
574
+
575
+ dw /= 2 # divide padding into 2 sides
576
+ dh /= 2
577
+ if labels.get('ratio_pad'):
578
+ labels['ratio_pad'] = (labels['ratio_pad'], (dw, dh)) # for evaluation
579
+
580
+ if shape[::-1] != new_unpad: # resize
581
+ img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
582
+ top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
583
+ left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
584
+ img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT,
585
+ value=(114, 114, 114)) # add border
586
+
587
+ if len(labels):
588
+ labels = self._update_labels(labels, ratio, dw, dh)
589
+ labels['img'] = img
590
+ labels['resized_shape'] = new_shape
591
+ return labels
592
+ else:
593
+ return img
594
+
595
+ def _update_labels(self, labels, ratio, padw, padh):
596
+ """Update labels."""
597
+ labels['instances'].convert_bbox(format='xyxy')
598
+ labels['instances'].denormalize(*labels['img'].shape[:2][::-1])
599
+ labels['instances'].scale(*ratio)
600
+ labels['instances'].add_padding(padw, padh)
601
+ return labels
602
+
603
+
604
+ class CopyPaste:
605
+
606
+ def __init__(self, p=0.5) -> None:
607
+ self.p = p
608
+
609
+ def __call__(self, labels):
610
+ """Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy)."""
611
+ im = labels['img']
612
+ cls = labels['cls']
613
+ h, w = im.shape[:2]
614
+ instances = labels.pop('instances')
615
+ instances.convert_bbox(format='xyxy')
616
+ instances.denormalize(w, h)
617
+ if self.p and len(instances.segments):
618
+ n = len(instances)
619
+ _, w, _ = im.shape # height, width, channels
620
+ im_new = np.zeros(im.shape, np.uint8)
621
+
622
+ # Calculate ioa first then select indexes randomly
623
+ ins_flip = deepcopy(instances)
624
+ ins_flip.fliplr(w)
625
+
626
+ ioa = bbox_ioa(ins_flip.bboxes, instances.bboxes) # intersection over area, (N, M)
627
+ indexes = np.nonzero((ioa < 0.30).all(1))[0] # (N, )
628
+ n = len(indexes)
629
+ for j in random.sample(list(indexes), k=round(self.p * n)):
630
+ cls = np.concatenate((cls, cls[[j]]), axis=0)
631
+ instances = Instances.concatenate((instances, ins_flip[[j]]), axis=0)
632
+ cv2.drawContours(im_new, instances.segments[[j]].astype(np.int32), -1, (1, 1, 1), cv2.FILLED)
633
+
634
+ result = cv2.flip(im, 1) # augment segments (flip left-right)
635
+ i = cv2.flip(im_new, 1).astype(bool)
636
+ im[i] = result[i] # cv2.imwrite('debug.jpg', im) # debug
637
+
638
+ labels['img'] = im
639
+ labels['cls'] = cls
640
+ labels['instances'] = instances
641
+ return labels
642
+
643
+
644
+ class Albumentations:
645
+ # YOLOv8 Albumentations class (optional, only used if package is installed)
646
+ def __init__(self, p=1.0):
647
+ """Initialize the transform object for YOLO bbox formatted params."""
648
+ self.p = p
649
+ self.transform = None
650
+ prefix = colorstr('albumentations: ')
651
+ try:
652
+ import albumentations as A
653
+
654
+ check_version(A.__version__, '1.0.3', hard=True) # version requirement
655
+
656
+ T = [
657
+ A.Blur(p=0.01),
658
+ A.MedianBlur(p=0.01),
659
+ A.ToGray(p=0.01),
660
+ A.CLAHE(p=0.01),
661
+ A.RandomBrightnessContrast(p=0.0),
662
+ A.RandomGamma(p=0.0),
663
+ A.ImageCompression(quality_lower=75, p=0.0)] # transforms
664
+ self.transform = A.Compose(T, bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
665
+
666
+ LOGGER.info(prefix + ', '.join(f'{x}'.replace('always_apply=False, ', '') for x in T if x.p))
667
+ except ImportError: # package not installed, skip
668
+ pass
669
+ except Exception as e:
670
+ LOGGER.info(f'{prefix}{e}')
671
+
672
+ def __call__(self, labels):
673
+ """Generates object detections and returns a dictionary with detection results."""
674
+ im = labels['img']
675
+ cls = labels['cls']
676
+ if len(cls):
677
+ labels['instances'].convert_bbox('xywh')
678
+ labels['instances'].normalize(*im.shape[:2][::-1])
679
+ bboxes = labels['instances'].bboxes
680
+ # TODO: add supports of segments and keypoints
681
+ if self.transform and random.random() < self.p:
682
+ new = self.transform(image=im, bboxes=bboxes, class_labels=cls) # transformed
683
+ if len(new['class_labels']) > 0: # skip update if no bbox in new im
684
+ labels['img'] = new['image']
685
+ labels['cls'] = np.array(new['class_labels'])
686
+ bboxes = np.array(new['bboxes'], dtype=np.float32)
687
+ labels['instances'].update(bboxes=bboxes)
688
+ return labels
689
+
690
+
691
+ # TODO: technically this is not an augmentation, maybe we should put this to another files
692
+ class Format:
693
+
694
+ def __init__(self,
695
+ bbox_format='xywh',
696
+ normalize=True,
697
+ return_mask=False,
698
+ return_keypoint=False,
699
+ mask_ratio=4,
700
+ mask_overlap=True,
701
+ batch_idx=True):
702
+ self.bbox_format = bbox_format
703
+ self.normalize = normalize
704
+ self.return_mask = return_mask # set False when training detection only
705
+ self.return_keypoint = return_keypoint
706
+ self.mask_ratio = mask_ratio
707
+ self.mask_overlap = mask_overlap
708
+ self.batch_idx = batch_idx # keep the batch indexes
709
+
710
+ def __call__(self, labels):
711
+ """Return formatted image, classes, bounding boxes & keypoints to be used by 'collate_fn'."""
712
+ img = labels.pop('img')
713
+ h, w = img.shape[:2]
714
+ cls = labels.pop('cls')
715
+ instances = labels.pop('instances')
716
+ instances.convert_bbox(format=self.bbox_format)
717
+ instances.denormalize(w, h)
718
+ nl = len(instances)
719
+
720
+ if self.return_mask:
721
+ if nl:
722
+ masks, instances, cls = self._format_segments(instances, cls, w, h)
723
+ masks = torch.from_numpy(masks)
724
+ else:
725
+ masks = torch.zeros(1 if self.mask_overlap else nl, img.shape[0] // self.mask_ratio,
726
+ img.shape[1] // self.mask_ratio)
727
+ labels['masks'] = masks
728
+ if self.normalize:
729
+ instances.normalize(w, h)
730
+ labels['img'] = self._format_img(img)
731
+ labels['cls'] = torch.from_numpy(cls) if nl else torch.zeros(nl)
732
+ labels['bboxes'] = torch.from_numpy(instances.bboxes) if nl else torch.zeros((nl, 4))
733
+ if self.return_keypoint:
734
+ labels['keypoints'] = torch.from_numpy(instances.keypoints)
735
+ # Then we can use collate_fn
736
+ if self.batch_idx:
737
+ labels['batch_idx'] = torch.zeros(nl)
738
+ return labels
739
+
740
+ def _format_img(self, img):
741
+ """Format the image for YOLOv5 from Numpy array to PyTorch tensor."""
742
+ if len(img.shape) < 3:
743
+ img = np.expand_dims(img, -1)
744
+ img = np.ascontiguousarray(img.transpose(2, 0, 1)[::-1])
745
+ img = torch.from_numpy(img)
746
+ return img
747
+
748
+ def _format_segments(self, instances, cls, w, h):
749
+ """convert polygon points to bitmap."""
750
+ segments = instances.segments
751
+ if self.mask_overlap:
752
+ masks, sorted_idx = polygons2masks_overlap((h, w), segments, downsample_ratio=self.mask_ratio)
753
+ masks = masks[None] # (640, 640) -> (1, 640, 640)
754
+ instances = instances[sorted_idx]
755
+ cls = cls[sorted_idx]
756
+ else:
757
+ masks = polygons2masks((h, w), segments, color=1, downsample_ratio=self.mask_ratio)
758
+
759
+ return masks, instances, cls
760
+
761
+
762
+ def v8_transforms(dataset, imgsz, hyp, stretch=False):
763
+ """Convert images to a size suitable for YOLOv8 training."""
764
+ pre_transform = Compose([
765
+ Mosaic(dataset, imgsz=imgsz, p=hyp.mosaic),
766
+ CopyPaste(p=hyp.copy_paste),
767
+ RandomPerspective(
768
+ degrees=hyp.degrees,
769
+ translate=hyp.translate,
770
+ scale=hyp.scale,
771
+ shear=hyp.shear,
772
+ perspective=hyp.perspective,
773
+ pre_transform=None if stretch else LetterBox(new_shape=(imgsz, imgsz)),
774
+ )])
775
+ flip_idx = dataset.data.get('flip_idx', None) # for keypoints augmentation
776
+ if dataset.use_keypoints:
777
+ kpt_shape = dataset.data.get('kpt_shape', None)
778
+ if flip_idx is None and hyp.fliplr > 0.0:
779
+ hyp.fliplr = 0.0
780
+ LOGGER.warning("WARNING ⚠️ No 'flip_idx' array defined in data.yaml, setting augmentation 'fliplr=0.0'")
781
+ elif flip_idx and (len(flip_idx) != kpt_shape[0]):
782
+ raise ValueError(f'data.yaml flip_idx={flip_idx} length must be equal to kpt_shape[0]={kpt_shape[0]}')
783
+
784
+ return Compose([
785
+ pre_transform,
786
+ MixUp(dataset, pre_transform=pre_transform, p=hyp.mixup),
787
+ Albumentations(p=1.0),
788
+ RandomHSV(hgain=hyp.hsv_h, sgain=hyp.hsv_s, vgain=hyp.hsv_v),
789
+ RandomFlip(direction='vertical', p=hyp.flipud),
790
+ RandomFlip(direction='horizontal', p=hyp.fliplr, flip_idx=flip_idx)]) # transforms
791
+
792
+
793
+ # Classification augmentations -----------------------------------------------------------------------------------------
794
+ def classify_transforms(size=224, mean=(0.0, 0.0, 0.0), std=(1.0, 1.0, 1.0)): # IMAGENET_MEAN, IMAGENET_STD
795
+ # Transforms to apply if albumentations not installed
796
+ if not isinstance(size, int):
797
+ raise TypeError(f'classify_transforms() size {size} must be integer, not (list, tuple)')
798
+ if any(mean) or any(std):
799
+ return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(mean, std, inplace=True)])
800
+ else:
801
+ return T.Compose([CenterCrop(size), ToTensor()])
802
+
803
+
804
+ def hsv2colorjitter(h, s, v):
805
+ """Map HSV (hue, saturation, value) jitter into ColorJitter values (brightness, contrast, saturation, hue)"""
806
+ return v, v, s, h
807
+
808
+
809
+ def classify_albumentations(
810
+ augment=True,
811
+ size=224,
812
+ scale=(0.08, 1.0),
813
+ hflip=0.5,
814
+ vflip=0.0,
815
+ hsv_h=0.015, # image HSV-Hue augmentation (fraction)
816
+ hsv_s=0.7, # image HSV-Saturation augmentation (fraction)
817
+ hsv_v=0.4, # image HSV-Value augmentation (fraction)
818
+ mean=(0.0, 0.0, 0.0), # IMAGENET_MEAN
819
+ std=(1.0, 1.0, 1.0), # IMAGENET_STD
820
+ auto_aug=False,
821
+ ):
822
+ # YOLOv8 classification Albumentations (optional, only used if package is installed)
823
+ prefix = colorstr('albumentations: ')
824
+ try:
825
+ import albumentations as A
826
+ from albumentations.pytorch import ToTensorV2
827
+
828
+ check_version(A.__version__, '1.0.3', hard=True) # version requirement
829
+ if augment: # Resize and crop
830
+ T = [A.RandomResizedCrop(height=size, width=size, scale=scale)]
831
+ if auto_aug:
832
+ # TODO: implement AugMix, AutoAug & RandAug in albumentations
833
+ LOGGER.info(f'{prefix}auto augmentations are currently not supported')
834
+ else:
835
+ if hflip > 0:
836
+ T += [A.HorizontalFlip(p=hflip)]
837
+ if vflip > 0:
838
+ T += [A.VerticalFlip(p=vflip)]
839
+ if any((hsv_h, hsv_s, hsv_v)):
840
+ T += [A.ColorJitter(*hsv2colorjitter(hsv_h, hsv_s, hsv_v))] # brightness, contrast, saturation, hue
841
+ else: # Use fixed crop for eval set (reproducibility)
842
+ T = [A.SmallestMaxSize(max_size=size), A.CenterCrop(height=size, width=size)]
843
+ T += [A.Normalize(mean=mean, std=std), ToTensorV2()] # Normalize and convert to Tensor
844
+ LOGGER.info(prefix + ', '.join(f'{x}'.replace('always_apply=False, ', '') for x in T if x.p))
845
+ return A.Compose(T)
846
+
847
+ except ImportError: # package not installed, skip
848
+ pass
849
+ except Exception as e:
850
+ LOGGER.info(f'{prefix}{e}')
851
+
852
+
853
+ class ClassifyLetterBox:
854
+ # YOLOv8 LetterBox class for image preprocessing, i.e. T.Compose([LetterBox(size), ToTensor()])
855
+ def __init__(self, size=(640, 640), auto=False, stride=32):
856
+ """Resizes image and crops it to center with max dimensions 'h' and 'w'."""
857
+ super().__init__()
858
+ self.h, self.w = (size, size) if isinstance(size, int) else size
859
+ self.auto = auto # pass max size integer, automatically solve for short side using stride
860
+ self.stride = stride # used with auto
861
+
862
+ def __call__(self, im): # im = np.array HWC
863
+ imh, imw = im.shape[:2]
864
+ r = min(self.h / imh, self.w / imw) # ratio of new/old
865
+ h, w = round(imh * r), round(imw * r) # resized image
866
+ hs, ws = (math.ceil(x / self.stride) * self.stride for x in (h, w)) if self.auto else self.h, self.w
867
+ top, left = round((hs - h) / 2 - 0.1), round((ws - w) / 2 - 0.1)
868
+ im_out = np.full((self.h, self.w, 3), 114, dtype=im.dtype)
869
+ im_out[top:top + h, left:left + w] = cv2.resize(im, (w, h), interpolation=cv2.INTER_LINEAR)
870
+ return im_out
871
+
872
+
873
+ class CenterCrop:
874
+ # YOLOv8 CenterCrop class for image preprocessing, i.e. T.Compose([CenterCrop(size), ToTensor()])
875
+ def __init__(self, size=640):
876
+ """Converts an image from numpy array to PyTorch tensor."""
877
+ super().__init__()
878
+ self.h, self.w = (size, size) if isinstance(size, int) else size
879
+
880
+ def __call__(self, im): # im = np.array HWC
881
+ imh, imw = im.shape[:2]
882
+ m = min(imh, imw) # min dimension
883
+ top, left = (imh - m) // 2, (imw - m) // 2
884
+ return cv2.resize(im[top:top + m, left:left + m], (self.w, self.h), interpolation=cv2.INTER_LINEAR)
885
+
886
+
887
+ class ToTensor:
888
+ # YOLOv8 ToTensor class for image preprocessing, i.e. T.Compose([LetterBox(size), ToTensor()])
889
+ def __init__(self, half=False):
890
+ """Initialize YOLOv8 ToTensor object with optional half-precision support."""
891
+ super().__init__()
892
+ self.half = half
893
+
894
+ def __call__(self, im): # im = np.array HWC in BGR order
895
+ im = np.ascontiguousarray(im.transpose((2, 0, 1))[::-1]) # HWC to CHW -> BGR to RGB -> contiguous
896
+ im = torch.from_numpy(im) # to torch
897
+ im = im.half() if self.half else im.float() # uint8 to fp16/32
898
+ im /= 255.0 # 0-255 to 0.0-1.0
899
+ return im