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.

@@ -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 _numpy_to_bytesio_png(seg_imgs: np.ndarray) -> Generator[BinaryIO, None, None]:
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
- seg_img (np.ndarray): The segmentation image with dimensions (height, width, #frames).
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
- seg_imgs = seg_imgs.astype(np.uint8)
34
- for i in range(seg_imgs.shape[2]):
35
- img = seg_imgs[:, :, i]
36
- img = Image.fromarray(img).convert('L')
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
- img.save(img_bytes, format='PNG')
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
- segs_imgs = file_path # (#frames, height, width) or (height, width)
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
- segs_imgs = segs_imgs.transpose(1, 0, 2) if segs_imgs.ndim == 3 else segs_imgs.transpose(1, 0)
52
- nframes = segs_imgs.shape[2] if segs_imgs.ndim == 3 else 1
53
- fios = AnnotationAPIHandler._numpy_to_bytesio_png(segs_imgs)
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
- # The if is correct. The image is already in a different shape than nifty images.
60
- segs_imgs = segs_imgs.transpose(1, 0, 2) if segs_imgs.ndim == 3 else segs_imgs.transpose(1, 0)
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
- if transpose_segmentation:
66
- with Image.open(file_path) as img:
67
- segs_imgs = np.array(img).transpose(1, 0)
68
- fios = AnnotationAPIHandler._numpy_to_bytesio_png(segs_imgs)
69
- else:
70
- fios = (open(file_path, 'rb') for _ in range(1))
71
- nframes = 1
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: Optional[str | dict[int, str]] = None,
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 or a dictionary mapping pixel values to names.
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
- img = np.array(Image.open(fio))
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(uniq_vals) == 1 and uniq_vals[0] == 0:
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
- fio.seek(0)
133
- # TODO: Optimize this. It is not necessary to open the image twice.
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
- segnames = AnnotationAPIHandler._get_segmentation_names(uniq_vals, names=name)
136
- segs_generator = AnnotationAPIHandler._split_segmentations(img, uniq_vals, fio)
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: str | dict[int, str] | None = None,
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 | None = None,
252
- fio: IO | None = None,
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 is not None and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
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
- if file_path is not None:
289
- # Handle volume upload
290
- if upload_volume:
291
- if frame_index is not None:
292
- _LOGGER.warning("frame_index parameter ignored when upload_volume=True")
293
-
294
- return await self._upload_volume_segmentation_async(
295
- resource_id=resource_id,
296
- file_path=file_path,
297
- name=name,
298
- imported_from=imported_from,
299
- author_email=author_email,
300
- worklist_id=worklist_id,
301
- model_id=model_id,
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
- if frame_index is None:
310
- frame_index = list(range(nframes))
311
-
312
- annotids = []
313
- for fidx, f in zip(frame_index, fios):
314
- frame_annotids = await self._upload_single_frame_segmentation_async(
315
- resource_id=resource_id,
316
- frame_index=fidx,
317
- fio=f,
318
- name=name,
319
- imported_from=imported_from,
320
- author_email=author_email,
321
- discard_empty_segmentations=discard_empty_segmentations,
322
- worklist_id=worklist_id,
323
- model_id=model_id
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=frame_index,
336
- fio=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
- raise ValueError("Either file_path or fio must be provided")
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: Optional[str | dict[int, str]] = None,
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 must have the shape (height, width, #frames) or (height, width).
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 (Optional[Union[str, Dict[int, str]]]): The name of the segmentation or a dictionary mapping pixel values to names.
368
- example: {1: 'Femur', 2: 'Tibia'}. For NIfTI files, only string names are supported.
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: The segmentation unique id.
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 len(frame_index) != nframes:
421
- raise ValueError("Do not provide frame_index for images of multiple frames.")
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._upload_segmentations_async(resource_id,
427
- fio=f,
428
- name=name,
429
- frame_index=fidx,
430
- imported_from=imported_from,
431
- author_email=author_email,
432
- discard_empty_segmentations=discard_empty_segmentations,
433
- worklist_id=worklist_id,
434
- model_id=model_id)
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 (Optional[str]): The annotation type. See :class:`~datamint.dto.annotation_dto.AnnotationType`.
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 _get_segmentation_names(uniq_vals: np.ndarray,
966
- names: Optional[str | dict[int, str]] = None
967
- ) -> list[str]:
968
- uniq_vals = uniq_vals[uniq_vals != 0]
969
- if names is None:
970
- names = 'seg'
971
- if isinstance(names, str):
972
- if len(uniq_vals) == 1:
973
- return [names]
974
- return [f'{names}_{v}' for v in uniq_vals]
975
- if isinstance(names, dict):
976
- for v in uniq_vals:
977
- new_name = names.get(v, names.get('default', None))
978
- if new_name is None:
979
- raise ValueError(f"Value {v} not found in names dictionary." +
980
- f" Provide a name for {v} or use 'default' key to provide a prefix.")
981
- return [names.get(v, names.get('default', '')+'_'+str(v)) for v in uniq_vals]
982
- raise ValueError("names must be a string or a dictionary.")
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 _split_segmentations(img: np.ndarray,
986
- uniq_vals: np.ndarray,
987
- f: IO,
988
- ) -> Generator[BytesIO, None, None]:
989
- # remove zero from uniq_vals
990
- uniq_vals = uniq_vals[uniq_vals != 0]
991
-
992
- for v in uniq_vals:
993
- img_v = (img == v).astype(np.uint8)
994
-
995
- f = BytesIO()
996
- Image.fromarray(img_v*255).convert('RGB').save(f, format='PNG')
997
- f.seek(0)
998
- yield f
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.5.5
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=N8WFk-oO84fBKH9t-R1DW5J7hnxQxcz-zxgLuMkNbwA,47766
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.5.5.dist-info/METADATA,sha256=o6BFPA7OS3SSPqflC85pJ_2Q7pETUtoZInY97B2Dxm8,4065
27
- datamint-1.5.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
- datamint-1.5.5.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
29
- datamint-1.5.5.dist-info/RECORD,,
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,,