datamint 1.5.5__py3-none-any.whl → 1.6.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.
Potentially problematic release.
This version of datamint might be problematic. Click here for more details.
- datamint/apihandler/annotation_api_handler.py +231 -147
- {datamint-1.5.5.dist-info → datamint-1.6.0.dist-info}/METADATA +1 -1
- {datamint-1.5.5.dist-info → datamint-1.6.0.dist-info}/RECORD +5 -5
- {datamint-1.5.5.dist-info → datamint-1.6.0.dist-info}/WHEEL +0 -0
- {datamint-1.5.5.dist-info → datamint-1.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -17,25 +17,57 @@ import json
|
|
|
17
17
|
|
|
18
18
|
_LOGGER = logging.getLogger(__name__)
|
|
19
19
|
_USER_LOGGER = logging.getLogger('user_logger')
|
|
20
|
+
MAX_NUMBER_DISTINCT_COLORS = 2048 # Maximum number of distinct colors in a segmentation image
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class AnnotationAPIHandler(BaseAPIHandler):
|
|
23
24
|
@staticmethod
|
|
24
|
-
def
|
|
25
|
+
def _normalize_segmentation_array(seg_imgs: np.ndarray) -> np.ndarray:
|
|
25
26
|
"""
|
|
27
|
+
Normalize segmentation array to a consistent format.
|
|
28
|
+
|
|
26
29
|
Args:
|
|
27
|
-
|
|
30
|
+
seg_imgs: Input segmentation array in various formats: (height, width, #frames), (height, width), (3, height, width, #frames).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
np.ndarray: Shape (#channels, height, width, #frames)
|
|
28
34
|
"""
|
|
35
|
+
if seg_imgs.ndim == 4:
|
|
36
|
+
return seg_imgs # .transpose(1, 2, 0, 3)
|
|
29
37
|
|
|
38
|
+
# Handle grayscale segmentations
|
|
30
39
|
if seg_imgs.ndim == 2:
|
|
40
|
+
# Add frame dimension: (height, width) -> (height, width, 1)
|
|
31
41
|
seg_imgs = seg_imgs[..., None]
|
|
42
|
+
if seg_imgs.ndim == 3:
|
|
43
|
+
# (height, width, #frames)
|
|
44
|
+
seg_imgs = seg_imgs[np.newaxis, ...] # Add channel dimension: (1, height, width, #frames)
|
|
45
|
+
|
|
46
|
+
return seg_imgs
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _numpy_to_bytesio_png(seg_imgs: np.ndarray) -> Generator[BinaryIO, None, None]:
|
|
50
|
+
"""
|
|
51
|
+
Convert normalized segmentation images to PNG BytesIO objects.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
seg_imgs: Normalized segmentation array in shape (channels, height, width, frames).
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
Yields:
|
|
57
|
+
BinaryIO: PNG image data as BytesIO objects
|
|
58
|
+
"""
|
|
59
|
+
# PIL RGB format is: (height, width, channels)
|
|
60
|
+
if seg_imgs.shape[0] not in [1, 3, 4]:
|
|
61
|
+
raise ValueError(f"Unsupported number of channels: {seg_imgs.shape[0]}. Expected 1 or 3")
|
|
62
|
+
nframes = seg_imgs.shape[3]
|
|
63
|
+
for i in range(nframes):
|
|
64
|
+
img = seg_imgs[:, :, :, i].astype(np.uint8)
|
|
65
|
+
if img.shape[0] == 1:
|
|
66
|
+
pil_img = Image.fromarray(img[0]).convert('RGB')
|
|
67
|
+
else:
|
|
68
|
+
pil_img = Image.fromarray(img.transpose(1, 2, 0))
|
|
37
69
|
img_bytes = BytesIO()
|
|
38
|
-
|
|
70
|
+
pil_img.save(img_bytes, format='PNG')
|
|
39
71
|
img_bytes.seek(0)
|
|
40
72
|
yield img_bytes
|
|
41
73
|
|
|
@@ -46,29 +78,42 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
46
78
|
raise ValueError(f"Unsupported file type: {type(file_path)}")
|
|
47
79
|
|
|
48
80
|
if isinstance(file_path, np.ndarray):
|
|
49
|
-
|
|
81
|
+
normalized_imgs = AnnotationAPIHandler._normalize_segmentation_array(file_path)
|
|
82
|
+
# normalized_imgs shape: (3, height, width, #frames)
|
|
83
|
+
|
|
84
|
+
# Apply transpose if requested
|
|
50
85
|
if transpose_segmentation:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
# (channels, height, width, frames) -> (channels, width, height, frames)
|
|
87
|
+
normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
|
|
88
|
+
|
|
89
|
+
nframes = normalized_imgs.shape[3]
|
|
90
|
+
fios = AnnotationAPIHandler._numpy_to_bytesio_png(normalized_imgs)
|
|
91
|
+
|
|
54
92
|
elif file_path.endswith('.nii') or file_path.endswith('.nii.gz'):
|
|
55
93
|
segs_imgs = nib.load(file_path).get_fdata()
|
|
56
94
|
if segs_imgs.ndim != 3 and segs_imgs.ndim != 2:
|
|
57
95
|
raise ValueError(f"Invalid segmentation shape: {segs_imgs.shape}")
|
|
96
|
+
|
|
97
|
+
# Normalize and apply transpose
|
|
98
|
+
normalized_imgs = AnnotationAPIHandler._normalize_segmentation_array(segs_imgs)
|
|
58
99
|
if not transpose_segmentation:
|
|
59
|
-
#
|
|
60
|
-
|
|
100
|
+
# Apply default NIfTI transpose
|
|
101
|
+
# (channels, width, height, frames) -> (channels, height, width, frames)
|
|
102
|
+
normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
|
|
103
|
+
|
|
104
|
+
nframes = normalized_imgs.shape[3]
|
|
105
|
+
fios = AnnotationAPIHandler._numpy_to_bytesio_png(normalized_imgs)
|
|
61
106
|
|
|
62
|
-
fios = AnnotationAPIHandler._numpy_to_bytesio_png(segs_imgs)
|
|
63
|
-
nframes = segs_imgs.shape[2] if segs_imgs.ndim == 3 else 1
|
|
64
107
|
elif file_path.endswith('.png'):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
with Image.open(file_path) as img:
|
|
109
|
+
img_array = np.array(img)
|
|
110
|
+
normalized_imgs = AnnotationAPIHandler._normalize_segmentation_array(img_array)
|
|
111
|
+
|
|
112
|
+
if transpose_segmentation:
|
|
113
|
+
normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
|
|
114
|
+
|
|
115
|
+
fios = AnnotationAPIHandler._numpy_to_bytesio_png(normalized_imgs)
|
|
116
|
+
nframes = 1
|
|
72
117
|
else:
|
|
73
118
|
raise ValueError(f"Unsupported file format of '{file_path}'")
|
|
74
119
|
|
|
@@ -91,9 +136,9 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
91
136
|
|
|
92
137
|
async def _upload_single_frame_segmentation_async(self,
|
|
93
138
|
resource_id: str,
|
|
94
|
-
frame_index: int,
|
|
139
|
+
frame_index: int | None,
|
|
95
140
|
fio: IO,
|
|
96
|
-
name:
|
|
141
|
+
name: dict[int, str] | dict[tuple, str],
|
|
97
142
|
imported_from: Optional[str] = None,
|
|
98
143
|
author_email: Optional[str] = None,
|
|
99
144
|
discard_empty_segmentations: bool = True,
|
|
@@ -107,7 +152,8 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
107
152
|
resource_id: The resource unique id.
|
|
108
153
|
frame_index: The frame index for the segmentation.
|
|
109
154
|
fio: File-like object containing the segmentation image.
|
|
110
|
-
name: The name of the segmentation
|
|
155
|
+
name: The name of the segmentation, a dictionary mapping pixel values to names,
|
|
156
|
+
or a dictionary mapping RGB tuples to names.
|
|
111
157
|
imported_from: The imported from value.
|
|
112
158
|
author_email: The author email.
|
|
113
159
|
discard_empty_segmentations: Whether to discard empty segmentations.
|
|
@@ -119,21 +165,29 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
119
165
|
"""
|
|
120
166
|
try:
|
|
121
167
|
try:
|
|
122
|
-
|
|
168
|
+
img_pil = Image.open(fio)
|
|
169
|
+
img_array = np.array(img_pil) # shape: (height, width, channels)
|
|
170
|
+
# Returns a list of (count, color) tuples
|
|
171
|
+
unique_vals = img_pil.getcolors(maxcolors=MAX_NUMBER_DISTINCT_COLORS)
|
|
172
|
+
# convert to list of RGB tuples
|
|
173
|
+
if unique_vals is None:
|
|
174
|
+
raise ValueError(f'Number of unique colors exceeds {MAX_NUMBER_DISTINCT_COLORS}.')
|
|
175
|
+
unique_vals = [color for count, color in unique_vals]
|
|
176
|
+
# Remove black/transparent pixels
|
|
177
|
+
black_pixel = (0, 0, 0)
|
|
178
|
+
unique_vals = [rgb for rgb in unique_vals if rgb != black_pixel]
|
|
123
179
|
|
|
124
|
-
# Check that frame is not empty
|
|
125
|
-
uniq_vals = np.unique(img)
|
|
126
180
|
if discard_empty_segmentations:
|
|
127
|
-
if len(
|
|
128
|
-
msg = f"Discarding empty segmentation for frame {frame_index}"
|
|
181
|
+
if len(unique_vals) == 0:
|
|
182
|
+
msg = f"Discarding empty RGB segmentation for frame {frame_index}"
|
|
129
183
|
_LOGGER.debug(msg)
|
|
130
184
|
_USER_LOGGER.debug(msg)
|
|
131
185
|
return []
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
segnames = AnnotationAPIHandler._get_segmentation_names_rgb(unique_vals, names=name)
|
|
187
|
+
segs_generator = AnnotationAPIHandler._split_rgb_segmentations(img_array, unique_vals)
|
|
134
188
|
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
fio.seek(0)
|
|
190
|
+
# TODO: Optimize this. It is not necessary to open the image twice.
|
|
137
191
|
|
|
138
192
|
# Create annotations
|
|
139
193
|
annotations: list[CreateAnnotationDto] = []
|
|
@@ -174,7 +228,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
174
228
|
resp = await self._run_request_async(request_params)
|
|
175
229
|
if 'error' in resp:
|
|
176
230
|
raise DatamintException(resp['error'])
|
|
177
|
-
|
|
178
231
|
return annotids
|
|
179
232
|
finally:
|
|
180
233
|
fio.close()
|
|
@@ -184,7 +237,7 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
184
237
|
async def _upload_volume_segmentation_async(self,
|
|
185
238
|
resource_id: str,
|
|
186
239
|
file_path: str | np.ndarray,
|
|
187
|
-
name:
|
|
240
|
+
name: dict[int, str] | dict[tuple, str],
|
|
188
241
|
imported_from: Optional[str] = None,
|
|
189
242
|
author_email: Optional[str] = None,
|
|
190
243
|
worklist_id: Optional[str] = None,
|
|
@@ -210,9 +263,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
210
263
|
Raises:
|
|
211
264
|
ValueError: If name is not a string or file format is unsupported for volume upload.
|
|
212
265
|
"""
|
|
213
|
-
if name is None:
|
|
214
|
-
name = 'volume_segmentation'
|
|
215
|
-
|
|
216
266
|
# Prepare file for upload
|
|
217
267
|
if isinstance(file_path, str):
|
|
218
268
|
if file_path.endswith('.nii') or file_path.endswith('.nii.gz'):
|
|
@@ -248,9 +298,8 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
248
298
|
async def _upload_segmentations_async(self,
|
|
249
299
|
resource_id: str,
|
|
250
300
|
frame_index: int | None,
|
|
251
|
-
file_path: str | np.ndarray
|
|
252
|
-
|
|
253
|
-
name: Optional[str | dict[int, str]] = None,
|
|
301
|
+
file_path: str | np.ndarray,
|
|
302
|
+
name: dict[int, str] | dict[tuple, str],
|
|
254
303
|
imported_from: Optional[str] = None,
|
|
255
304
|
author_email: Optional[str] = None,
|
|
256
305
|
discard_empty_segmentations: bool = True,
|
|
@@ -266,7 +315,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
266
315
|
resource_id: The resource unique id.
|
|
267
316
|
frame_index: The frame index or None for multiple frames.
|
|
268
317
|
file_path: Path to segmentation file or numpy array.
|
|
269
|
-
fio: File-like object containing segmentation data.
|
|
270
318
|
name: The name of the segmentation or mapping of pixel values to names.
|
|
271
319
|
imported_from: The imported from value.
|
|
272
320
|
author_email: The author email.
|
|
@@ -280,60 +328,44 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
280
328
|
List of annotation IDs created.
|
|
281
329
|
"""
|
|
282
330
|
if upload_volume == 'auto':
|
|
283
|
-
if file_path
|
|
331
|
+
if isinstance(file_path, str) and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
|
|
284
332
|
upload_volume = True
|
|
285
333
|
else:
|
|
286
334
|
upload_volume = False
|
|
287
335
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
transpose_segmentation=transpose_segmentation
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
# Handle frame-by-frame upload (existing logic)
|
|
306
|
-
nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(
|
|
307
|
-
file_path, transpose_segmentation=transpose_segmentation
|
|
336
|
+
# Handle volume upload
|
|
337
|
+
if upload_volume:
|
|
338
|
+
if frame_index is not None:
|
|
339
|
+
_LOGGER.warning("frame_index parameter ignored when upload_volume=True")
|
|
340
|
+
|
|
341
|
+
return await self._upload_volume_segmentation_async(
|
|
342
|
+
resource_id=resource_id,
|
|
343
|
+
file_path=file_path,
|
|
344
|
+
name=name,
|
|
345
|
+
imported_from=imported_from,
|
|
346
|
+
author_email=author_email,
|
|
347
|
+
worklist_id=worklist_id,
|
|
348
|
+
model_id=model_id,
|
|
349
|
+
transpose_segmentation=transpose_segmentation
|
|
308
350
|
)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
325
|
-
annotids.extend(frame_annotids)
|
|
326
|
-
return annotids
|
|
327
|
-
|
|
328
|
-
# Handle single file-like object
|
|
329
|
-
if fio is not None:
|
|
330
|
-
if upload_volume:
|
|
331
|
-
raise ValueError("upload_volume=True is not supported when providing fio parameter")
|
|
332
|
-
|
|
333
|
-
return await self._upload_single_frame_segmentation_async(
|
|
351
|
+
|
|
352
|
+
# Handle frame-by-frame upload (existing logic)
|
|
353
|
+
nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(
|
|
354
|
+
file_path, transpose_segmentation=transpose_segmentation
|
|
355
|
+
)
|
|
356
|
+
if frame_index is None:
|
|
357
|
+
frames_indices = list(range(nframes))
|
|
358
|
+
elif isinstance(frame_index, int):
|
|
359
|
+
frames_indices = [frame_index]
|
|
360
|
+
else:
|
|
361
|
+
raise ValueError("frame_index must be an int or None")
|
|
362
|
+
|
|
363
|
+
annotids = []
|
|
364
|
+
for fidx, f in zip(frames_indices, fios):
|
|
365
|
+
frame_annotids = await self._upload_single_frame_segmentation_async(
|
|
334
366
|
resource_id=resource_id,
|
|
335
|
-
frame_index=
|
|
336
|
-
fio=
|
|
367
|
+
frame_index=fidx,
|
|
368
|
+
fio=f,
|
|
337
369
|
name=name,
|
|
338
370
|
imported_from=imported_from,
|
|
339
371
|
author_email=author_email,
|
|
@@ -341,13 +373,30 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
341
373
|
worklist_id=worklist_id,
|
|
342
374
|
model_id=model_id
|
|
343
375
|
)
|
|
376
|
+
annotids.extend(frame_annotids)
|
|
377
|
+
return annotids
|
|
344
378
|
|
|
345
|
-
|
|
379
|
+
@staticmethod
|
|
380
|
+
def standardize_segmentation_names(name: str | dict[int, str] | dict[tuple, str] | None) -> dict[tuple[int, int, int], str]:
|
|
381
|
+
if name is None:
|
|
382
|
+
return {}
|
|
383
|
+
elif isinstance(name, str):
|
|
384
|
+
return {'default': name}
|
|
385
|
+
elif isinstance(name, dict):
|
|
386
|
+
name = {
|
|
387
|
+
tuple(k) if isinstance(k, (list, tuple)) else k if isinstance(k, str) else (k, k, k): v
|
|
388
|
+
for k, v in name.items()
|
|
389
|
+
}
|
|
390
|
+
if 'default' not in name:
|
|
391
|
+
name['default'] = None
|
|
392
|
+
return name
|
|
393
|
+
else:
|
|
394
|
+
raise ValueError("Invalid name format")
|
|
346
395
|
|
|
347
396
|
def upload_segmentations(self,
|
|
348
397
|
resource_id: str,
|
|
349
398
|
file_path: str | np.ndarray,
|
|
350
|
-
name:
|
|
399
|
+
name: str | dict[int, str] | dict[tuple, str] | None = None,
|
|
351
400
|
frame_index: int | list[int] | None = None,
|
|
352
401
|
imported_from: Optional[str] = None,
|
|
353
402
|
author_email: Optional[str] = None,
|
|
@@ -362,30 +411,46 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
362
411
|
Args:
|
|
363
412
|
resource_id (str): The resource unique id.
|
|
364
413
|
file_path (str|np.ndarray): The path to the segmentation file or a numpy array.
|
|
365
|
-
If a numpy array is provided, it
|
|
414
|
+
If a numpy array is provided, it can have the shape:
|
|
415
|
+
- (height, width, #frames) or (height, width) for grayscale segmentations
|
|
416
|
+
- (3, height, width, #frames) for RGB segmentations
|
|
366
417
|
For NIfTI files (.nii/.nii.gz), the entire volume is uploaded as a single segmentation.
|
|
367
|
-
name
|
|
368
|
-
|
|
418
|
+
name: The name of the segmentation.
|
|
419
|
+
Can be:
|
|
420
|
+
- str: Single name for all segmentations
|
|
421
|
+
- dict[int, str]: Mapping pixel values to names for grayscale segmentations
|
|
422
|
+
- dict[tuple[int, int, int], str]: Mapping RGB tuples to names for RGB segmentations
|
|
423
|
+
Example: {(255, 0, 0): 'Red_Region', (0, 255, 0): 'Green_Region'}
|
|
369
424
|
frame_index (int | list[int]): The frame index of the segmentation.
|
|
370
425
|
If a list, it must have the same length as the number of frames in the segmentation.
|
|
371
426
|
If None, it is assumed that the segmentations are in sequential order starting from 0.
|
|
372
427
|
This parameter is ignored for NIfTI files as they are treated as volume segmentations.
|
|
373
428
|
discard_empty_segmentations (bool): Whether to discard empty segmentations or not.
|
|
374
|
-
This is ignored for NIfTI files.
|
|
375
429
|
|
|
376
430
|
Returns:
|
|
377
|
-
str:
|
|
431
|
+
List[str]: List of segmentation unique ids.
|
|
378
432
|
|
|
379
433
|
Raises:
|
|
380
434
|
ResourceNotFoundError: If the resource does not exists or the segmentation is invalid.
|
|
381
435
|
|
|
382
436
|
Example:
|
|
437
|
+
>>> # Grayscale segmentation
|
|
383
438
|
>>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.png', 'SegmentationName')
|
|
439
|
+
>>>
|
|
440
|
+
>>> # RGB segmentation with numpy array
|
|
441
|
+
>>> seg_data = np.random.randint(0, 3, size=(3, 2140, 1760, 1), dtype=np.uint8)
|
|
442
|
+
>>> rgb_names = {(1, 0, 0): 'Red_Region', (0, 1, 0): 'Green_Region', (0, 0, 1): 'Blue_Region'}
|
|
443
|
+
>>> api_handler.upload_segmentation(resource_id, seg_data, rgb_names)
|
|
444
|
+
>>>
|
|
445
|
+
>>> # Volume segmentation
|
|
384
446
|
>>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.nii.gz', 'VolumeSegmentation')
|
|
385
447
|
"""
|
|
448
|
+
|
|
386
449
|
if isinstance(file_path, str) and not os.path.exists(file_path):
|
|
387
450
|
raise FileNotFoundError(f"File {file_path} not found.")
|
|
388
451
|
|
|
452
|
+
name = AnnotationAPIHandler.standardize_segmentation_names(name)
|
|
453
|
+
|
|
389
454
|
# Handle NIfTI files specially - upload as single volume
|
|
390
455
|
if isinstance(file_path, str) and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
|
|
391
456
|
_LOGGER.info(f"Uploading NIfTI segmentation file: {file_path}")
|
|
@@ -407,33 +472,32 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
407
472
|
)
|
|
408
473
|
return loop.run_until_complete(task)
|
|
409
474
|
# All other file types are converted to multiple PNGs and uploaded frame by frame.
|
|
410
|
-
if isinstance(frame_index, int):
|
|
411
|
-
frame_index = [frame_index]
|
|
412
475
|
|
|
413
|
-
loop = asyncio.get_event_loop()
|
|
414
476
|
to_run = []
|
|
415
477
|
# Generate IOs for the segmentations.
|
|
416
478
|
nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(file_path,
|
|
417
479
|
transpose_segmentation=transpose_segmentation)
|
|
418
480
|
if frame_index is None:
|
|
419
481
|
frame_index = list(range(nframes))
|
|
420
|
-
elif
|
|
421
|
-
|
|
422
|
-
|
|
482
|
+
elif isinstance(frame_index, int):
|
|
483
|
+
frame_index = [frame_index]
|
|
484
|
+
if len(frame_index) != nframes:
|
|
485
|
+
raise ValueError(f'Expected {nframes} frame_index values, but got {len(frame_index)}.')
|
|
423
486
|
|
|
424
487
|
# For each frame, create the annotations and upload the segmentations.
|
|
425
488
|
for fidx, f in zip(frame_index, fios):
|
|
426
|
-
task = self.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
489
|
+
task = self._upload_single_frame_segmentation_async(resource_id,
|
|
490
|
+
fio=f,
|
|
491
|
+
name=name,
|
|
492
|
+
frame_index=fidx,
|
|
493
|
+
imported_from=imported_from,
|
|
494
|
+
author_email=author_email,
|
|
495
|
+
discard_empty_segmentations=discard_empty_segmentations,
|
|
496
|
+
worklist_id=worklist_id,
|
|
497
|
+
model_id=model_id)
|
|
435
498
|
to_run.append(task)
|
|
436
499
|
|
|
500
|
+
loop = asyncio.get_event_loop()
|
|
437
501
|
ret = loop.run_until_complete(asyncio.gather(*to_run))
|
|
438
502
|
# merge the results in a single list
|
|
439
503
|
ret = [item for sublist in ret for item in sublist]
|
|
@@ -831,7 +895,7 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
831
895
|
|
|
832
896
|
Args:
|
|
833
897
|
resource_id (Optional[str]): The resource unique id.
|
|
834
|
-
annotation_type (
|
|
898
|
+
annotation_type (AnnotationType | str | None): The annotation type. See :class:`~datamint.dto.annotation_dto.AnnotationType`.
|
|
835
899
|
annotator_email (Optional[str]): The annotator email.
|
|
836
900
|
date_from (Optional[date]): The start date.
|
|
837
901
|
date_to (Optional[date]): The end date.
|
|
@@ -843,7 +907,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
843
907
|
Returns:
|
|
844
908
|
Generator[dict, None, None]: A generator of dictionaries with the annotations information.
|
|
845
909
|
"""
|
|
846
|
-
# TODO: create annotation_type enum
|
|
847
910
|
|
|
848
911
|
if annotation_type is not None and isinstance(annotation_type, AnnotationType):
|
|
849
912
|
annotation_type = annotation_type.value
|
|
@@ -962,40 +1025,61 @@ class AnnotationAPIHandler(BaseAPIHandler):
|
|
|
962
1025
|
self._run_request(request_params)
|
|
963
1026
|
|
|
964
1027
|
@staticmethod
|
|
965
|
-
def
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1028
|
+
def _get_segmentation_names_rgb(uniq_rgb_vals: list[tuple[int, int, int]],
|
|
1029
|
+
names: dict[tuple[int, int, int], str]
|
|
1030
|
+
) -> list[str]:
|
|
1031
|
+
"""
|
|
1032
|
+
Generate segmentation names for RGB combinations.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
uniq_rgb_vals: List of unique RGB combinations as (R,G,B) tuples
|
|
1036
|
+
names: Name mapping for RGB combinations
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
List of segmentation names
|
|
1040
|
+
"""
|
|
1041
|
+
result = []
|
|
1042
|
+
for rgb_tuple in uniq_rgb_vals:
|
|
1043
|
+
seg_name = names.get(rgb_tuple, names.get('default', f'seg_{"_".join(map(str, rgb_tuple))}'))
|
|
1044
|
+
if seg_name is None:
|
|
1045
|
+
if rgb_tuple[0] == rgb_tuple[1] and rgb_tuple[1] == rgb_tuple[2]:
|
|
1046
|
+
msg = f"Provide a name for {rgb_tuple} or {rgb_tuple[0]} or use 'default' key."
|
|
1047
|
+
else:
|
|
1048
|
+
msg = f"Provide a name for {rgb_tuple} or use 'default' key."
|
|
1049
|
+
raise ValueError(f"RGB combination {rgb_tuple} not found in names dictionary. " +
|
|
1050
|
+
msg)
|
|
1051
|
+
# If using default prefix, append RGB values
|
|
1052
|
+
# if rgb_tuple not in names and 'default' in names:
|
|
1053
|
+
# seg_name = f"{seg_name}_{'_'.join(map(str, rgb_tuple))}"
|
|
1054
|
+
result.append(seg_name)
|
|
1055
|
+
return result
|
|
983
1056
|
|
|
984
1057
|
@staticmethod
|
|
985
|
-
def
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1058
|
+
def _split_rgb_segmentations(img: np.ndarray,
|
|
1059
|
+
uniq_rgb_vals: list[tuple[int, int, int]]
|
|
1060
|
+
) -> Generator[BytesIO, None, None]:
|
|
1061
|
+
"""
|
|
1062
|
+
Split RGB segmentations into individual binary masks.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
img: RGB image array of shape (height, width, channels)
|
|
1066
|
+
uniq_rgb_vals: List of unique RGB combinations as (R,G,B) tuples
|
|
1067
|
+
|
|
1068
|
+
Yields:
|
|
1069
|
+
BytesIO objects containing individual segmentation masks
|
|
1070
|
+
"""
|
|
1071
|
+
for rgb_tuple in uniq_rgb_vals:
|
|
1072
|
+
# Create binary mask for this RGB combination
|
|
1073
|
+
rgb_array = np.array(rgb_tuple[:3]) # Ensure only R,G,B values
|
|
1074
|
+
mask = np.all(img[:, :, :3] == rgb_array, axis=2)
|
|
1075
|
+
|
|
1076
|
+
# Convert to uint8 and create PNG
|
|
1077
|
+
mask_img = (mask * 255).astype(np.uint8)
|
|
1078
|
+
|
|
1079
|
+
f_out = BytesIO()
|
|
1080
|
+
Image.fromarray(mask_img).convert('L').save(f_out, format='PNG')
|
|
1081
|
+
f_out.seek(0)
|
|
1082
|
+
yield f_out
|
|
999
1083
|
|
|
1000
1084
|
def delete_annotation(self, annotation_id: str | dict):
|
|
1001
1085
|
if isinstance(annotation_id, dict):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: datamint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
datamint/__init__.py,sha256=7rKCCsaa4RBRTIfuHB708rai1xwDHLtkFNFJGKYG5D4,757
|
|
2
|
-
datamint/apihandler/annotation_api_handler.py,sha256=
|
|
2
|
+
datamint/apihandler/annotation_api_handler.py,sha256=jEY0Ka5RikkD2435cDNQ59l3M4NSkOJ1NwRreWQYl4c,51616
|
|
3
3
|
datamint/apihandler/api_handler.py,sha256=cdVSddrFCKlF_BJ81LO1aJ0OP49rssjpNEFzJ6Q7YyY,384
|
|
4
4
|
datamint/apihandler/base_api_handler.py,sha256=XSxZEQEkbQpuixGDu_P9jbxUQht3Z3JgxaeiFKPkVDM,11690
|
|
5
5
|
datamint/apihandler/dto/annotation_dto.py,sha256=otCIesoqGBlbSOw4ErqFsXp2HwJsPNUQlkynQh_7pHg,7110
|
|
@@ -23,7 +23,7 @@ datamint/utils/io_utils.py,sha256=lKnUCJEip7W9Xj9wOWsTAA855HnKbjwQON1WjMGqJmM,73
|
|
|
23
23
|
datamint/utils/logging_utils.py,sha256=DvoA35ATYG3JTwfXEXYawDyKRfHeCrH0a9czfkmz8kM,1851
|
|
24
24
|
datamint/utils/torchmetrics.py,sha256=lwU0nOtsSWfebyp7dvjlAggaqXtj5ohSEUXOg3L0hJE,2837
|
|
25
25
|
datamint/utils/visualization.py,sha256=yaUVAOHar59VrGUjpAWv5eVvQSfztFG0eP9p5Vt3l-M,4470
|
|
26
|
-
datamint-1.
|
|
27
|
-
datamint-1.
|
|
28
|
-
datamint-1.
|
|
29
|
-
datamint-1.
|
|
26
|
+
datamint-1.6.0.dist-info/METADATA,sha256=F73Llyz1xUSDM5luVjsjL8EZwLP8VAcMV91vpi2BVqw,4065
|
|
27
|
+
datamint-1.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
28
|
+
datamint-1.6.0.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
|
|
29
|
+
datamint-1.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|