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.
- deepvisiontools/__init__.py +16 -0
- deepvisiontools/config/__init__.py +3 -0
- deepvisiontools/config/config.py +176 -0
- deepvisiontools/config/errors.py +12 -0
- deepvisiontools/data/__init__.py +24 -0
- deepvisiontools/data/additional_augmentations.py +201 -0
- deepvisiontools/data/augmentation_class.py +37 -0
- deepvisiontools/data/batch_augmentations.py +211 -0
- deepvisiontools/data/data_reader/__init__.py +3 -0
- deepvisiontools/data/data_reader/readers.py +490 -0
- deepvisiontools/data/dataset.py +357 -0
- deepvisiontools/data/errors.py +23 -0
- deepvisiontools/formats/__init__.py +46 -0
- deepvisiontools/formats/base_data.py +802 -0
- deepvisiontools/formats/base_formats.py +321 -0
- deepvisiontools/formats/errors.py +167 -0
- deepvisiontools/formats/formats.py +282 -0
- deepvisiontools/formats/utils.py +174 -0
- deepvisiontools/inference/__init__.py +4 -0
- deepvisiontools/inference/patchifier.py +343 -0
- deepvisiontools/inference/predictor.py +208 -0
- deepvisiontools/metrics/__init__.py +50 -0
- deepvisiontools/metrics/available_metrics.py +215 -0
- deepvisiontools/metrics/base_metric.py +436 -0
- deepvisiontools/metrics/func.py +34 -0
- deepvisiontools/metrics/matcher.py +99 -0
- deepvisiontools/metrics/utils.py +39 -0
- deepvisiontools/models/__init__.py +14 -0
- deepvisiontools/models/basemodel.py +100 -0
- deepvisiontools/models/mask2former/__init__.py +3 -0
- deepvisiontools/models/mask2former/mask2former.py +285 -0
- deepvisiontools/models/smp/__init__.py +0 -0
- deepvisiontools/models/smp/errors.py +4 -0
- deepvisiontools/models/smp/smp.py +149 -0
- deepvisiontools/models/yolov8/__init__.py +15 -0
- deepvisiontools/models/yolov8/errors.py +25 -0
- deepvisiontools/models/yolov8/utils.py +92 -0
- deepvisiontools/models/yolov8/yolov8.py +302 -0
- deepvisiontools/models/yolov8seg/__init__.py +4 -0
- deepvisiontools/models/yolov8seg/errors.py +14 -0
- deepvisiontools/models/yolov8seg/utils.py +135 -0
- deepvisiontools/models/yolov8seg/yolov8seg.py +370 -0
- deepvisiontools/preprocessing/__init__.py +19 -0
- deepvisiontools/preprocessing/image.py +79 -0
- deepvisiontools/preprocessing/preprocessing.py +65 -0
- deepvisiontools/train/__init__.py +3 -0
- deepvisiontools/train/trainer.py +335 -0
- deepvisiontools/utils/__init__.py +3 -0
- deepvisiontools/utils/errors.py +0 -0
- deepvisiontools/utils/visualization.py +318 -0
- deepvisiontools-0.0.1.dist-info/LICENSE.txt +661 -0
- deepvisiontools-0.0.1.dist-info/METADATA +725 -0
- deepvisiontools-0.0.1.dist-info/RECORD +55 -0
- deepvisiontools-0.0.1.dist-info/WHEEL +5 -0
- 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,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)
|