deepvisiontools 0.0.1__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 (55) hide show
  1. deepvisiontools/__init__.py +16 -0
  2. deepvisiontools/config/__init__.py +3 -0
  3. deepvisiontools/config/config.py +176 -0
  4. deepvisiontools/config/errors.py +12 -0
  5. deepvisiontools/data/__init__.py +24 -0
  6. deepvisiontools/data/additional_augmentations.py +201 -0
  7. deepvisiontools/data/augmentation_class.py +37 -0
  8. deepvisiontools/data/batch_augmentations.py +211 -0
  9. deepvisiontools/data/data_reader/__init__.py +3 -0
  10. deepvisiontools/data/data_reader/readers.py +490 -0
  11. deepvisiontools/data/dataset.py +357 -0
  12. deepvisiontools/data/errors.py +23 -0
  13. deepvisiontools/formats/__init__.py +46 -0
  14. deepvisiontools/formats/base_data.py +802 -0
  15. deepvisiontools/formats/base_formats.py +321 -0
  16. deepvisiontools/formats/errors.py +167 -0
  17. deepvisiontools/formats/formats.py +282 -0
  18. deepvisiontools/formats/utils.py +174 -0
  19. deepvisiontools/inference/__init__.py +4 -0
  20. deepvisiontools/inference/patchifier.py +343 -0
  21. deepvisiontools/inference/predictor.py +208 -0
  22. deepvisiontools/metrics/__init__.py +50 -0
  23. deepvisiontools/metrics/available_metrics.py +215 -0
  24. deepvisiontools/metrics/base_metric.py +436 -0
  25. deepvisiontools/metrics/func.py +34 -0
  26. deepvisiontools/metrics/matcher.py +99 -0
  27. deepvisiontools/metrics/utils.py +39 -0
  28. deepvisiontools/models/__init__.py +14 -0
  29. deepvisiontools/models/basemodel.py +100 -0
  30. deepvisiontools/models/mask2former/__init__.py +3 -0
  31. deepvisiontools/models/mask2former/mask2former.py +285 -0
  32. deepvisiontools/models/smp/__init__.py +0 -0
  33. deepvisiontools/models/smp/errors.py +4 -0
  34. deepvisiontools/models/smp/smp.py +149 -0
  35. deepvisiontools/models/yolov8/__init__.py +15 -0
  36. deepvisiontools/models/yolov8/errors.py +25 -0
  37. deepvisiontools/models/yolov8/utils.py +92 -0
  38. deepvisiontools/models/yolov8/yolov8.py +302 -0
  39. deepvisiontools/models/yolov8seg/__init__.py +4 -0
  40. deepvisiontools/models/yolov8seg/errors.py +14 -0
  41. deepvisiontools/models/yolov8seg/utils.py +135 -0
  42. deepvisiontools/models/yolov8seg/yolov8seg.py +370 -0
  43. deepvisiontools/preprocessing/__init__.py +19 -0
  44. deepvisiontools/preprocessing/image.py +79 -0
  45. deepvisiontools/preprocessing/preprocessing.py +65 -0
  46. deepvisiontools/train/__init__.py +3 -0
  47. deepvisiontools/train/trainer.py +335 -0
  48. deepvisiontools/utils/__init__.py +3 -0
  49. deepvisiontools/utils/errors.py +0 -0
  50. deepvisiontools/utils/visualization.py +318 -0
  51. deepvisiontools-0.0.1.dist-info/LICENSE.txt +661 -0
  52. deepvisiontools-0.0.1.dist-info/METADATA +725 -0
  53. deepvisiontools-0.0.1.dist-info/RECORD +55 -0
  54. deepvisiontools-0.0.1.dist-info/WHEEL +5 -0
  55. deepvisiontools-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,211 @@
1
+ from deepvisiontools.formats import BaseFormat, BatchedFormat, SemanticMaskFormat
2
+ import torch
3
+ from torch import Tensor
4
+ from abc import ABC, abstractmethod
5
+ from typing import Tuple, Union, Literal, List
6
+ from random import shuffle, uniform
7
+ from deepvisiontools import Configuration
8
+ import itertools
9
+ from torchvision.transforms.v2 import Resize
10
+ import copy
11
+
12
+
13
+ class AbstractBatchAugmenter(ABC):
14
+ """Abstract class for augmentation within DataLoader (combine elements of batch together such as mosaic type augmentation)
15
+ Note : these augmentations always come after normal augmentations that are implemented in Dataset instead of dataloader for this one.
16
+ """
17
+
18
+ @abstractmethod
19
+ def get_new_batch(
20
+ self, images_batch: Tensor, targets_batch: BatchedFormat
21
+ ) -> Tuple[Tensor, BatchedFormat]:
22
+ pass
23
+
24
+
25
+ MOSAIC_CONVERTER = {
26
+ 1: (1, 1),
27
+ 2: (1, 2),
28
+ 4: (2, 2),
29
+ 6: (2, 3),
30
+ 8: (2, 4),
31
+ 9: (3, 3),
32
+ 12: (3, 4),
33
+ }
34
+
35
+
36
+ class MosaicBatchAugmenter(AbstractBatchAugmenter):
37
+ """This Batch augmentation generate a mosaic containing n images from a batch (mix some patch of images / targets into one image).
38
+ If the number of image is larger than batch size shift to smaller possibility (for e.g. n = 4 batch_size=3 -> n becomes 2).
39
+ if number of image to be mixed is smaller than batch_size, create new mosaics if possible : for e.g batchsize = 5, n=2 -> generate 2 mosaics from the first 2 images, then an additional 2 images with remaining, and finally the remaining is 1.
40
+ The remaining images are left untouched
41
+
42
+ Args:
43
+ mixed_img_numb (``Literal[1, 2, 4, 6, 8, 9, 12]``, **optional**): Number of img per mosaic. Defaults to 2.
44
+ probability (``float``, **optional**): _description_. Defaults to 0.5.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ mixed_img_numb: Literal[1, 2, 4, 6, 8, 9, 12] = 2,
50
+ probability: float = 0.5,
51
+ ):
52
+
53
+ assert mixed_img_numb in list(
54
+ MOSAIC_CONVERTER.keys()
55
+ ), f"Wrong argument in MosaicBatchAugmenter, mixed_img_num must be one of [1, 2, 4, 6, 8, 9, 12], got {mixed_img_numb}"
56
+ self.mixed_img_number = mixed_img_numb
57
+ self._used_mixed_img_nb = mixed_img_numb
58
+ self.probability = probability
59
+ self._device = Configuration().device
60
+ self._imgs_shape = None
61
+ self._mosaic_shape = None
62
+ self._resizer = None
63
+
64
+ def _adapt_to_batch(self, images_batch: Tensor):
65
+ assert (
66
+ images_batch.dim() == 4
67
+ ), f"To use MosaicBatchAugmenter, batches of images must have dim=4 (N, 3, H, W), got {images_batch.shape}"
68
+ num_img = images_batch.shape[0]
69
+ h, w = images_batch.shape[-2:]
70
+ option_list = [1, 2, 4, 6, 8, 9, 12]
71
+ # adapt batch size to option of nb of mixed img
72
+ while self._used_mixed_img_nb > num_img:
73
+ self._used_mixed_img_nb = option_list[
74
+ option_list.index(self._used_mixed_img_nb) - 1
75
+ ]
76
+ self._imgs_shape = images_batch.shape[-2:]
77
+ # adapt mosaic shape to largest between h and w of images
78
+ m_shape = MOSAIC_CONVERTER[self._used_mixed_img_nb]
79
+ if h > w:
80
+ m_shape = (max(m_shape), min(m_shape))
81
+ self._mosaic_shape = m_shape
82
+ self._resizer = Resize((h, w))
83
+
84
+ def _reset(self):
85
+ self._used_mixed_img_nb = self.mixed_img_number
86
+ self._mosaic_shape = None
87
+ self._resizer = None
88
+
89
+ def _get_mixing_index(self, images_batch: Tensor) -> Tuple[Tensor, Tensor]:
90
+ num_imgs = images_batch.shape[0]
91
+ index = list(range(num_imgs))
92
+ shuffle(index)
93
+ kept_index = index
94
+ mixing_index = []
95
+ while len(kept_index) >= self._used_mixed_img_nb:
96
+ mixing_index.append(kept_index[: self._used_mixed_img_nb])
97
+ kept_index = kept_index[self._used_mixed_img_nb :]
98
+ return mixing_index, kept_index
99
+
100
+ def _generate_batchs_lists(
101
+ self, images_batch: Tensor, targets_batch: BatchedFormat
102
+ ) -> Tuple[List[Tensor], List[BatchedFormat], Tensor, BatchedFormat]:
103
+ mixing_index, kept_indexes = self._get_mixing_index(images_batch)
104
+ # split batch into the ones that are kept and the ones to be mixed.
105
+ if kept_indexes != []:
106
+ images2keep: Tensor = images_batch[kept_indexes]
107
+ targets2keep: BatchedFormat = targets_batch[kept_indexes]
108
+ else:
109
+ images2keep = torch.empty([0])
110
+ targets2keep = BatchedFormat([])
111
+
112
+ images_batches_to_mix: list = [
113
+ images_batch[mix_batch] for mix_batch in mixing_index
114
+ ] # list of Tensor of shape [n_mix, h, w]
115
+ targets_batches_to_mix: list = [
116
+ targets_batch[mix_batch] for mix_batch in mixing_index
117
+ ] # list of corresponding BatchedFormats
118
+ return images_batches_to_mix, targets_batches_to_mix, images2keep, targets2keep
119
+
120
+ def _get_crop_coords_lists(self):
121
+ h, w = self._imgs_shape
122
+ h_divider, w_divider = self._mosaic_shape
123
+ crop_coords_list = []
124
+ h_step = float(h) / h_divider
125
+ w_step = float(w) / w_divider
126
+ for i, j in itertools.product(list(range(h_divider)), list(range(w_divider))):
127
+ t_c, l_c, h_c, w_c = i * h_step, j * w_step, h_step, w_step
128
+ crop_coords_list.append((round(t_c), round(l_c), round(h_c), round(w_c)))
129
+ return crop_coords_list
130
+
131
+ def _mix_batch(
132
+ self,
133
+ batch_img: Tensor,
134
+ batch_targ: BatchedFormat,
135
+ crops_coords: List[Tuple[int, int, int, int]],
136
+ ):
137
+ num_imgs = batch_img.shape[0]
138
+ cycle = list(range(num_imgs)) # use this for all imgs/targs permutations
139
+ cycles = []
140
+ for i in range(len(cycle)):
141
+ first_elem = cycle.pop(0)
142
+ cycle.append(first_elem)
143
+ cycles.append(copy.deepcopy(cycle))
144
+
145
+ new_batch_img = []
146
+ new_batch_targs = []
147
+ for cy in cycles:
148
+ new_img, new_tar = self._get_img_targ_from_cycle_of_crops(
149
+ batch_img, batch_targ, cy, crops_coords
150
+ )
151
+ new_batch_img.append(copy.deepcopy(new_img))
152
+ new_batch_targs.append(copy.deepcopy(new_tar))
153
+ return torch.stack(new_batch_img), BatchedFormat(new_batch_targs)
154
+
155
+ def _get_img_targ_from_cycle_of_crops(
156
+ self,
157
+ batch_img: Tensor,
158
+ batch_targ: BatchedFormat,
159
+ cycle: list,
160
+ crops_coords: list,
161
+ ):
162
+ list_imgs = [batch_img[i] for i in cycle]
163
+ list_targs = BatchedFormat([batch_targ.formats[i] for i in cycle])
164
+ new_img = torch.zeros((3, *self._imgs_shape))
165
+ new_targ: BaseFormat = type(list_targs.formats[0]).empty(
166
+ canvas_size=list_targs.formats[0].canvas_size
167
+ )
168
+ ori_h, ori_w = new_targ.canvas_size
169
+ if isinstance(new_targ, SemanticMaskFormat):
170
+ new_targ = new_targ.generate_scores_from_mask()
171
+ for im, tar, crop_coord in zip(list_imgs, list_targs, crops_coords):
172
+ t, l, h, w = crop_coord
173
+ new_img[:, t : t + h, l : l + w] = im[:, t : t + h, l : l + w]
174
+ crop_targ = tar.crop(t, l, h, w)[0]
175
+ t_pad, l_pad, r_pad, b_pad = t, l, ori_w - l - w, ori_h - t - h
176
+ crop_targ = crop_targ.pad(t_pad, l_pad, r_pad, b_pad)[0]
177
+ if isinstance(crop_targ, SemanticMaskFormat):
178
+ crop_targ = crop_targ.generate_scores_from_mask()
179
+ new_targ += crop_targ
180
+ return new_img, new_targ
181
+
182
+ def get_new_batch(self, images_batch: Tensor, targets_batch: BatchedFormat):
183
+ # ======= Prepare
184
+ self._adapt_to_batch(images_batch)
185
+ # if proba fails or if image nb to mix is 1, don't do mixing
186
+ if uniform(0, 1) > self.probability or self._used_mixed_img_nb == 1:
187
+ return images_batch, targets_batch
188
+ # get to be mixed imgs and targs as well as to be kept images and targs
189
+ images_batches_to_mix, targets_batches_to_mix, images2keep, targets2keep = (
190
+ self._generate_batchs_lists(images_batch, targets_batch)
191
+ )
192
+ # ======= Handle mixing
193
+ crops_coords = self._get_crop_coords_lists()
194
+ final_batchs_img_mixed = []
195
+ final_batchs_tars_mixed = []
196
+ for batch_img, batch_targ in zip(images_batches_to_mix, targets_batches_to_mix):
197
+ new_img, new_targ = self._mix_batch(batch_img, batch_targ, crops_coords)
198
+ final_batchs_img_mixed.append(new_img)
199
+ final_batchs_tars_mixed.append(new_targ)
200
+ final_imgs_mixed = torch.cat(final_batchs_img_mixed).to(Configuration().device)
201
+ final_targs_mixed = BatchedFormat.cat(final_batchs_tars_mixed)
202
+ # aggragate untouched and mixed
203
+ if targets2keep.formats != []:
204
+ new_img_fullbatch = torch.cat([final_imgs_mixed, images2keep])
205
+ new_tar_fullbatch = BatchedFormat.cat([final_targs_mixed, targets2keep])
206
+ else:
207
+ new_img_fullbatch = final_imgs_mixed
208
+ new_tar_fullbatch = final_targs_mixed
209
+ # ======= reset for futu uses
210
+ self._reset()
211
+ return new_img_fullbatch, new_tar_fullbatch
@@ -0,0 +1,3 @@
1
+ from deepvisiontools.data.data_reader.readers import BaseReader, CocoReader
2
+
3
+ __all__ = ("BaseReader", "CocoReader")
@@ -0,0 +1,490 @@
1
+ from typing import Union, Tuple, Dict, List, Any
2
+ from pathlib import Path
3
+ from abc import ABC, abstractmethod
4
+ from deepvisiontools.formats import (
5
+ BaseFormat,
6
+ BboxFormat,
7
+ InstanceMaskFormat,
8
+ SemanticMaskFormat,
9
+ )
10
+ from deepvisiontools.formats.base_data import (
11
+ InstanceMaskData,
12
+ BboxData,
13
+ SemanticMaskData,
14
+ )
15
+ from deepvisiontools import Configuration
16
+ import torch
17
+ import numpy as np
18
+ from torch import Tensor
19
+ import json
20
+ import cv2
21
+ from tqdm import tqdm
22
+ from deepvisiontools.preprocessing import load_mask
23
+
24
+ ANNOTATION_TYPE_DICT = {
25
+ "json": "file",
26
+ "png": "mask",
27
+ "tif": "mask",
28
+ "tiff": "mask",
29
+ "jpg": "mask",
30
+ "jpeg": "mask",
31
+ }
32
+
33
+ SUPPORTED_IMAGE_EXTENSIONS = [
34
+ "png",
35
+ "PNG",
36
+ "jpg",
37
+ "JPG",
38
+ "jpeg",
39
+ "JPEG",
40
+ "tif",
41
+ "TIF",
42
+ "tiff",
43
+ "TIFF",
44
+ ]
45
+
46
+
47
+ class BaseReader(ABC):
48
+ """Base class for readers. __len__ and __getitem__ methods must be implemented in concrete class.
49
+ Your concrete class must implement concrete category_id property that returns Dict[int, str] where int is label and str category name.
50
+ Your concrete class must have a class attribute describing annotation file type ("json" for json file, "png" for image etc.)
51
+ You must implement export_annotation and group_export methods in concrete classes
52
+ See CocoReader class for concrete implementation
53
+ """
54
+
55
+ @property
56
+ def annotation_file_type(cls):
57
+ raise NotImplementedError
58
+
59
+ @abstractmethod
60
+ def __getitem__(self, index: int) -> Tuple[str, BaseFormat]:
61
+ """From a given index ([0, N-1] where N is the number of images) returns image name, Format
62
+
63
+ Args:
64
+ index (``int``): Index of image from 0 to N-1
65
+
66
+ Returns:
67
+ ``Tuple[str, Format]``:
68
+ - returns image name (ex: something.jpg), target as Format
69
+ """
70
+ pass
71
+
72
+ @abstractmethod
73
+ def __len__(self) -> int:
74
+ pass
75
+
76
+ @abstractmethod
77
+ def export_annotation(self, image: Tensor, annotation: BaseFormat):
78
+ pass
79
+
80
+ @abstractmethod
81
+ def group_export(
82
+ sub_anns_dir: Union[str, Path],
83
+ destination: Union[str, Path],
84
+ categories: Dict[int, str] = None,
85
+ ):
86
+ pass
87
+
88
+ @property
89
+ @abstractmethod
90
+ def category_ids(self) -> Dict[int, str]:
91
+ pass
92
+
93
+ @category_ids.setter
94
+ @abstractmethod
95
+ def category_ids(self, val):
96
+ pass
97
+
98
+
99
+ DEFAULT_COCO_ANNOT = "coco_annotations.json"
100
+
101
+ DEFAULT_SEMANTIC_ANNOT_PATH = "masks"
102
+
103
+
104
+ # TODO check everything
105
+ class SemanticReader(BaseReader):
106
+
107
+ annotation_file_type = "tiff" # used for export dataset only
108
+
109
+ def __init__(self, dataset_path: Union[str, Path]):
110
+ dataset_path = (
111
+ dataset_path if isinstance(dataset_path, Path) else Path(dataset_path)
112
+ )
113
+ # load all files paths (images and masks)
114
+ self.masks_path = dataset_path / DEFAULT_SEMANTIC_ANNOT_PATH
115
+ images_path = dataset_path / "images"
116
+ self.images_path = images_path
117
+ self.images = list(images_path.glob("*"))
118
+ self.images = [
119
+ f.name for f in self.images if f.suffix[1:] in SUPPORTED_IMAGE_EXTENSIONS
120
+ ]
121
+ self.images = sorted(self.images)
122
+ self.masks = list(self.masks_path.glob("*"))
123
+ self.masks = [
124
+ f for f in self.masks if f.suffix[1:] in SUPPORTED_IMAGE_EXTENSIONS
125
+ ]
126
+ self.masks = sorted(self.masks)
127
+ assert len(self.masks) == len(
128
+ self.images
129
+ ), "Not same number of masks and images. You must have the same."
130
+ self.category_ids = {
131
+ i + 1: str(i + 1) for i in range(Configuration().num_classes)
132
+ }
133
+
134
+ @property
135
+ def category_ids(self):
136
+ return self._category_ids
137
+
138
+ @category_ids.setter
139
+ def category_ids(self, val: Dict[int, str]):
140
+ self._category_ids = val
141
+
142
+ def __getitem__(self, index):
143
+ img_name = self.images[index]
144
+ assert (
145
+ Path(img_name).stem == self.masks[index].stem
146
+ ), f"In Reader: index {index} leads to different name for image and mask, got {img_name} and {self.masks[index].name}"
147
+ target = load_mask(self.masks[index])
148
+ target = SemanticMaskData(target)
149
+ target = SemanticMaskFormat(target)
150
+ return img_name, target
151
+
152
+ def __len__(self):
153
+ return len(self.images)
154
+
155
+ def export_annotation(
156
+ self, image_name, image, annotation: SemanticMaskFormat, cats
157
+ ):
158
+ assert isinstance(
159
+ annotation, SemanticMaskFormat
160
+ ), "In semanticreader : annotation must be SemanticMaskFormat"
161
+ mask = annotation.data.value
162
+ mask = mask.to("cpu")
163
+ return image_name, mask
164
+
165
+ def group_export(
166
+ self,
167
+ sub_anns_dir: Union[str, Path],
168
+ destination: Union[str, Path],
169
+ categories: Dict[int, str] = None,
170
+ ):
171
+ destination = "masks"
172
+ sub_anns_dir.rename(sub_anns_dir.parent / destination)
173
+
174
+
175
+ class CocoReader(BaseReader):
176
+ """Child class of BaseReader. Coco format reader class. Handles dataset with structure:
177
+
178
+ Dataset Name -> Image_dir, coco_annotations.json
179
+
180
+ Note : bboxes must be in XYWH format
181
+
182
+ Args:
183
+ annotation_path (Union[str, Path]): path to json file or to dataset directory.
184
+
185
+ Attributes
186
+ ----------
187
+
188
+ Attributes:
189
+ - annot_dict (``Dict[Any, Any]``): coco dict loaded.
190
+
191
+ Attributes
192
+ ----------
193
+
194
+ Properties:
195
+ - category_ids Dict[int, str]: label / category correspondance
196
+
197
+
198
+ **Methods**
199
+ """
200
+
201
+ annotation_file_type = "json"
202
+
203
+ def __init__(self, annotation_path: Union[str, Path]) -> None:
204
+
205
+ annotation_path = (
206
+ annotation_path
207
+ if isinstance(annotation_path, Path)
208
+ else Path(annotation_path)
209
+ )
210
+ if annotation_path.is_dir():
211
+ annotation_path = annotation_path / DEFAULT_COCO_ANNOT
212
+ with open(annotation_path) as f:
213
+ self.annot_dict = json.load(f)
214
+ # if items of annot_dict are dict convert them to lists
215
+ if isinstance(self.annot_dict["categories"], dict):
216
+ self.annot_dict["categories"] = [v for v in self.annot_dict["categories"]]
217
+ if isinstance(self.annot_dict["images"], dict):
218
+ self.annot_dict["images"] = [v for v in self.annot_dict["images"]]
219
+ if isinstance(self.annot_dict["annotations"], dict):
220
+ self.annot_dict["annotations"] = [v for v in self.annot_dict["annotations"]]
221
+ # compute reindexer dict for imgs
222
+ self._imgs_reindex_dict = {
223
+ i: im["id"] for i, im in enumerate(self.annot_dict["images"])
224
+ }
225
+ # compute reindexer dict for labels / categories
226
+ ordered_keys = [cat["name"] for cat in self.annot_dict["categories"]]
227
+ ordered_keys.sort()
228
+ self._category_converter = {}
229
+ for i, name in enumerate(ordered_keys):
230
+ for cat in self.annot_dict["categories"]:
231
+ if name == cat["name"]:
232
+ self._category_converter.update({i: cat})
233
+ self._label_converter = {
234
+ k["id"]: i for i, k in self._category_converter.items()
235
+ }
236
+ # generate a correspondance between new labels and cat name
237
+ self.category_ids = {i: k["name"] for i, k in self._category_converter.items()}
238
+
239
+ @property
240
+ def category_ids(self):
241
+ return self._category_ids
242
+
243
+ @category_ids.setter
244
+ def category_ids(self, val: Dict[int, str]):
245
+ self._category_ids = val
246
+
247
+ def get_img_anns(self, index: int) -> Tuple[str, Tuple[int, int], List[dict]]:
248
+ """return from index image as img name, spatial size as Tuple[int, int] (h, w) and all annotations for given image index
249
+
250
+ Args:
251
+ index (int)
252
+ Returns:
253
+ Tuple[str, Tuple[int, int], List[dict]]: img_name, spatial_size, list of coco anns
254
+ """
255
+ image = [
256
+ img
257
+ for img in self.annot_dict["images"]
258
+ if img["id"] == self._imgs_reindex_dict[index]
259
+ ]
260
+ spatial_size = (image[0]["height"], image[0]["width"])
261
+ image = image[0]["file_name"]
262
+ anns = [
263
+ ann
264
+ for ann in self.annot_dict["annotations"]
265
+ if ann["image_id"] == self._imgs_reindex_dict[index]
266
+ ]
267
+ return image, spatial_size, anns
268
+
269
+ def __getitem__(self, index: int) -> Tuple[str, BaseFormat]:
270
+ """From a given index ([0, N-1] where N is the number of images) returns image name, Format
271
+
272
+ Args:
273
+ index (``int``): Index of image from 0 to N-1
274
+
275
+ Returns:
276
+ ``Tuple[str, Format]``:
277
+ - returns image name (ex: something.jpg), target as Format
278
+ """
279
+ image_name, spatial_size, anns = self.get_img_anns(index)
280
+
281
+ # handle empty anns
282
+ if anns == []:
283
+ if Configuration().data_type == "bbox":
284
+ format = BboxFormat.empty(spatial_size)
285
+ if Configuration().data_type == "instance_mask":
286
+ format = InstanceMaskFormat.empty(spatial_size)
287
+ elif Configuration().data_type == "bbox":
288
+ labels = torch.tensor(
289
+ [
290
+ self._label_converter[ann["category_id"]]
291
+ for ann in anns
292
+ if "bbox" in ann.keys()
293
+ ]
294
+ )
295
+ bboxes = [ann["bbox"] for ann in anns if "bbox" in ann.keys()]
296
+ # handle empty
297
+ if bboxes == []:
298
+ format = BboxFormat.empty(spatial_size)
299
+ else:
300
+ bboxes = [torch.tensor(box)[None, :] for box in bboxes]
301
+ bboxes = BboxData(
302
+ torch.cat(bboxes), format="XYWH", canvas_size=spatial_size
303
+ )
304
+ format = BboxFormat(bboxes, labels)
305
+ elif Configuration().data_type == "instance_mask":
306
+ labels = torch.tensor(
307
+ [
308
+ self._label_converter[ann["category_id"]]
309
+ for ann in anns
310
+ if "segmentation" in ann.keys()
311
+ ]
312
+ )
313
+ if labels == []:
314
+ format = InstanceMaskFormat.empty(spatial_size)
315
+ else:
316
+ object_masks = [
317
+ self.segment2mask(ann, spatial_size)
318
+ for ann in anns
319
+ if "segmentation" in ann.keys()
320
+ ]
321
+ # reindex objects and stack them with id in [1 ... N_obj] range
322
+ object_masks = [m * (i + 1) for i, m in enumerate(object_masks)]
323
+ mask = np.max(np.stack(object_masks, axis=0), axis=0)
324
+ mask = torch.tensor(mask).long()
325
+ format = InstanceMaskFormat(InstanceMaskData(mask), labels)
326
+ return image_name, format
327
+
328
+ def segment2mask(
329
+ self, ann: Dict[Any, Any], spatial_size: Tuple[int, int]
330
+ ) -> np.ndarray:
331
+ """Convert segment to object mask.
332
+
333
+ Args:
334
+ ann (Dict[Any, Any]): coco format annotation dict
335
+ spatial_size (Tuple[int, int]): size of image as (h, w)
336
+
337
+ Returns:
338
+ np.ndarray: mask
339
+ """
340
+ segments = ann["segmentation"]
341
+ # handle RLE coding
342
+ if isinstance(segments, dict):
343
+ assert (
344
+ "counts" in segments.keys()
345
+ ), f"Segmentation {segments} is dict but not RLE encoded."
346
+ mask: np.ndarray = self.rleToMask(segments, spatial_size)
347
+ else:
348
+ mask = np.zeros((spatial_size[0], spatial_size[1], 3))
349
+ segments = [
350
+ np.array(seg, dtype=np.int32).reshape(-1, 2)
351
+ for seg in segments
352
+ if len(seg) > 4
353
+ ]
354
+
355
+ assert segments != [], "Empty annotation after filtering segment length"
356
+ for poly in segments:
357
+ cv2.drawContours(mask, [poly], -1, color=(1), thickness=-1)
358
+ mask = mask[:, :, 0]
359
+ mask: np.ndarray = mask.astype(np.uint64)
360
+ return mask
361
+
362
+ def rleToMask(self, rle: str, shape: Tuple[int, int]) -> np.ndarray:
363
+ """convert rle encoding to binary mask
364
+
365
+ Args:
366
+ rle (str)
367
+ shape (Tuple[int, int])
368
+
369
+ Returns:
370
+ np.ndarray: mask
371
+ """
372
+ rle = rle["counts"]
373
+ # force even number of elem
374
+ if not len(rle) % 2 == 0:
375
+ rle.append(0)
376
+ width, height = shape[:2]
377
+
378
+ mask = np.zeros(width * height).astype(np.uint64)
379
+
380
+ array = np.asarray(rle)
381
+ starts = array[0::2]
382
+ lengths = array[1::2]
383
+
384
+ current_position = 0
385
+ for i, start in enumerate(starts):
386
+ current_position += start
387
+ mask[current_position : int(current_position + lengths[i])] = 1
388
+ current_position += lengths[i]
389
+
390
+ return mask.reshape(height, width).T
391
+
392
+ def __len__(self) -> int:
393
+ return len(self.annot_dict["images"])
394
+
395
+ def export_annotation(
396
+ self,
397
+ image_name: str,
398
+ image: Tensor,
399
+ format: BaseFormat,
400
+ categories: Dict[int, str],
401
+ ) -> Tuple[str, Dict[Any, Any]]:
402
+ """from image, image name, categories and target (as BaseFormat) returns a writeable coco dict.
403
+
404
+ Args:
405
+ image_name (str)
406
+ image (Tensor)
407
+ format (BaseFormat)
408
+ categories (Dict[int, str]): Dict of label / category name correspondance
409
+
410
+ Returns:
411
+ Tuple[str, Dict[Any, Any]]: image_name, coco dict
412
+ """
413
+ format.device = "cpu"
414
+ images = [
415
+ {
416
+ "file_name": image_name,
417
+ "height": image.shape[-2],
418
+ "width": image.shape[-1],
419
+ "id": 0,
420
+ }
421
+ ]
422
+ categories = [{"name": k, "id": v} for v, k in categories.items()]
423
+ anns = []
424
+ for i in range(format.nb_object):
425
+ obj, _ = format[i]
426
+ # if no object, stop loop
427
+ if obj.nb_object == 0:
428
+ break
429
+ # retrieve mask/box and label
430
+ label = obj.labels.item()
431
+ obj.device = "cpu"
432
+ ann_dict = {"id": 0, "image_id": 0, "category_id": label}
433
+ if isinstance(obj, InstanceMaskFormat):
434
+ bboxes: BboxData = BboxData.from_mask(obj.data)
435
+ bboxes.format = "XYWH"
436
+ bboxes.device = "cpu"
437
+ bboxes: Tensor = bboxes.value
438
+ bboxes_coco = bboxes.numpy()[0]
439
+ bboxes_coco = [int(b) for b in bboxes_coco]
440
+ contours, _ = cv2.findContours(
441
+ obj.data.value.numpy().astype(np.uint8),
442
+ cv2.RETR_EXTERNAL,
443
+ cv2.CHAIN_APPROX_NONE,
444
+ )
445
+ contours = [c.reshape(-1).tolist() for c in contours]
446
+ ann_dict.update({"bbox": bboxes_coco, "segmentation": contours})
447
+ elif isinstance(obj, BboxFormat):
448
+ obj.data.format = "XYWH"
449
+ bboxes = obj.data.value
450
+ bboxes_coco = bboxes.numpy()[0]
451
+ bboxes_coco = [int(b) for b in bboxes_coco]
452
+ ann_dict.update({"bbox": bboxes_coco})
453
+ anns.append(ann_dict)
454
+ coco_dict = {"categories": categories, "images": images, "annotations": anns}
455
+ return image_name, coco_dict
456
+
457
+ def group_export(
458
+ self,
459
+ sub_anns_dir: Union[str, Path],
460
+ destination: Union[str, Path],
461
+ categories: Dict[int, str] = None,
462
+ ):
463
+ destination = destination / "coco_annotations.json"
464
+ categories = self.category_ids if categories == None else categories
465
+ """Final layer of export dataset function. Here looks at all individual json files and merge them to coco_annotations.json
466
+
467
+ Args:
468
+ sub_anns_dir (Union[str, Path])
469
+ destination (Union[str, Path])
470
+ categories (Dict[int, str])
471
+ """
472
+ categories = [{"name": k, "id": v} for v, k in categories.items()]
473
+ grouped_dict = {"categories": categories, "images": [], "annotations": []}
474
+ jsons_list = sub_anns_dir.glob("*.json")
475
+ jsons_list = [json.as_posix() for json in jsons_list]
476
+ img_idx = 0
477
+ ann_idx = 0
478
+ for j_file in tqdm(jsons_list, desc="Grouping jsons : "):
479
+ with open(j_file) as f:
480
+ d = json.load(f)
481
+ d["images"][0]["id"] = img_idx
482
+ grouped_dict["images"].append(d["images"][0])
483
+ for i in range(len(d["annotations"])):
484
+ d["annotations"][i]["id"] = ann_idx
485
+ d["annotations"][i]["image_id"] = img_idx
486
+ grouped_dict["annotations"].append(d["annotations"][i])
487
+ ann_idx += 1
488
+ img_idx += 1
489
+ with open(destination, "w") as f:
490
+ json.dump(grouped_dict, f)