datamint 1.4.1__py3-none-any.whl → 1.5.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.

Potentially problematic release.


This version of datamint might be problematic. Click here for more details.

@@ -13,6 +13,7 @@ from requests.exceptions import HTTPError
13
13
  from deprecated.sphinx import deprecated
14
14
  from .dto.annotation_dto import CreateAnnotationDto, LineGeometry, BoxGeometry, CoordinateSystem, AnnotationType
15
15
  import pydicom
16
+ import json
16
17
 
17
18
  _LOGGER = logging.getLogger(__name__)
18
19
  _USER_LOGGER = logging.getLogger('user_logger')
@@ -88,41 +89,39 @@ class AnnotationAPIHandler(BaseAPIHandler):
88
89
  raise DatamintException(r['error'])
89
90
  return resp
90
91
 
91
- async def _upload_segmentations_async(self,
92
- resource_id: str,
93
- frame_index: int | None,
94
- file_path: str | np.ndarray | None = None,
95
- fio: IO | None = None,
96
- name: Optional[str | dict[int, str]] = None,
97
- imported_from: Optional[str] = None,
98
- author_email: Optional[str] = None,
99
- discard_empty_segmentations: bool = True,
100
- worklist_id: Optional[str] = None,
101
- model_id: Optional[str] = None,
102
- transpose_segmentation: bool = False
103
- ) -> list[str]:
104
- if file_path is not None:
105
- nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(file_path,
106
- transpose_segmentation=transpose_segmentation)
107
- if frame_index is None:
108
- frame_index = list(range(nframes))
109
- annotids = []
110
- for fidx, f in zip(frame_index, fios):
111
- reti = await self._upload_segmentations_async(resource_id,
112
- fio=f,
113
- name=name,
114
- frame_index=fidx,
115
- imported_from=imported_from,
116
- author_email=author_email,
117
- discard_empty_segmentations=discard_empty_segmentations,
118
- worklist_id=worklist_id,
119
- model_id=model_id)
120
- annotids.extend(reti)
121
- return annotids
92
+ async def _upload_single_frame_segmentation_async(self,
93
+ resource_id: str,
94
+ frame_index: int,
95
+ fio: IO,
96
+ name: Optional[str | dict[int, str]] = None,
97
+ imported_from: Optional[str] = None,
98
+ author_email: Optional[str] = None,
99
+ discard_empty_segmentations: bool = True,
100
+ worklist_id: Optional[str] = None,
101
+ model_id: Optional[str] = None
102
+ ) -> list[str]:
103
+ """
104
+ Upload a single frame segmentation asynchronously.
105
+
106
+ Args:
107
+ resource_id: The resource unique id.
108
+ frame_index: The frame index for the segmentation.
109
+ fio: File-like object containing the segmentation image.
110
+ name: The name of the segmentation or a dictionary mapping pixel values to names.
111
+ imported_from: The imported from value.
112
+ author_email: The author email.
113
+ discard_empty_segmentations: Whether to discard empty segmentations.
114
+ worklist_id: The annotation worklist unique id.
115
+ model_id: The model unique id.
116
+
117
+ Returns:
118
+ List of annotation IDs created.
119
+ """
122
120
  try:
123
121
  try:
124
122
  img = np.array(Image.open(fio))
125
- ### Check that frame is not empty ###
123
+
124
+ # Check that frame is not empty
126
125
  uniq_vals = np.unique(img)
127
126
  if discard_empty_segmentations:
128
127
  if len(uniq_vals) == 1 and uniq_vals[0] == 0:
@@ -135,31 +134,38 @@ class AnnotationAPIHandler(BaseAPIHandler):
135
134
 
136
135
  segnames = AnnotationAPIHandler._get_segmentation_names(uniq_vals, names=name)
137
136
  segs_generator = AnnotationAPIHandler._split_segmentations(img, uniq_vals, fio)
137
+
138
+ # Create annotations
138
139
  annotations: list[CreateAnnotationDto] = []
139
140
  for segname in segnames:
140
- ann = CreateAnnotationDto(type='segmentation',
141
- identifier=segname,
142
- scope='frame',
143
- frame_index=frame_index,
144
- imported_from=imported_from,
145
- import_author=author_email,
146
- model_id=model_id,
147
- annotation_worklist_id=worklist_id)
141
+ ann = CreateAnnotationDto(
142
+ type='segmentation',
143
+ identifier=segname,
144
+ scope='frame',
145
+ frame_index=frame_index,
146
+ imported_from=imported_from,
147
+ import_author=author_email,
148
+ model_id=model_id,
149
+ annotation_worklist_id=worklist_id
150
+ )
148
151
  annotations.append(ann)
149
- # raise ValueError if there is multiple annotations with the same identifier, frame_index, scope and author
152
+
153
+ # Validate unique identifiers
150
154
  if len(annotations) != len(set([a.identifier for a in annotations])):
151
155
  raise ValueError(
152
- "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet.")
156
+ "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet."
157
+ )
153
158
 
154
159
  annotids = await self._upload_annotations_async(resource_id, annotations)
155
160
 
156
- ### Upload segmentation ###
161
+ # Upload segmentation files
157
162
  if len(annotids) != len(segnames):
158
163
  _LOGGER.warning(f"Number of uploaded annotations ({len(annotids)})" +
159
164
  f" does not match the number of annotations ({len(segnames)})")
160
- for annotid, segname, fio in zip(annotids, segnames, segs_generator):
165
+
166
+ for annotid, segname, fio_seg in zip(annotids, segnames, segs_generator):
161
167
  form = aiohttp.FormData()
162
- form.add_field('file', fio, filename=segname, content_type='image/png')
168
+ form.add_field('file', fio_seg, filename=segname, content_type='image/png')
163
169
  request_params = dict(
164
170
  method='POST',
165
171
  url=f'{self.root_url}/annotations/{resource_id}/annotations/{annotid}/file',
@@ -168,19 +174,182 @@ class AnnotationAPIHandler(BaseAPIHandler):
168
174
  resp = await self._run_request_async(request_params)
169
175
  if 'error' in resp:
170
176
  raise DatamintException(resp['error'])
171
- #######
177
+
178
+ return annotids
172
179
  finally:
173
180
  fio.close()
174
- _USER_LOGGER.info(f'Segmentations uploaded for resource {resource_id}')
175
- return annotids
176
181
  except ResourceNotFoundError:
177
182
  raise ResourceNotFoundError('resource', {'resource_id': resource_id})
178
183
 
184
+ async def _upload_volume_segmentation_async(self,
185
+ resource_id: str,
186
+ file_path: str | np.ndarray,
187
+ name: str | dict[int, str] | None = None,
188
+ imported_from: Optional[str] = None,
189
+ author_email: Optional[str] = None,
190
+ worklist_id: Optional[str] = None,
191
+ model_id: Optional[str] = None,
192
+ transpose_segmentation: bool = False
193
+ ) -> list[str]:
194
+ """
195
+ Upload a volume segmentation as a single file asynchronously.
196
+
197
+ Args:
198
+ resource_id: The resource unique id.
199
+ file_path: Path to segmentation file or numpy array.
200
+ name: The name of the segmentation (string only for volumes).
201
+ imported_from: The imported from value.
202
+ author_email: The author email.
203
+ worklist_id: The annotation worklist unique id.
204
+ model_id: The model unique id.
205
+ transpose_segmentation: Whether to transpose the segmentation.
206
+
207
+ Returns:
208
+ List of annotation IDs created.
209
+
210
+ Raises:
211
+ ValueError: If name is not a string or file format is unsupported for volume upload.
212
+ """
213
+ if name is None:
214
+ name = 'volume_segmentation'
215
+
216
+ # Prepare file for upload
217
+ if isinstance(file_path, str):
218
+ if file_path.endswith('.nii') or file_path.endswith('.nii.gz'):
219
+ # Upload NIfTI file directly
220
+ with open(file_path, 'rb') as f:
221
+ filename = os.path.basename(file_path)
222
+ form = aiohttp.FormData()
223
+ form.add_field('file', f, filename=filename, content_type='application/x-nifti')
224
+ model_id = 'c9daf156-5335-4cb3-b374-5b3a776e0025'
225
+ if model_id is not None:
226
+ form.add_field('model_id', model_id) # Add model_id if provided
227
+ if worklist_id is not None:
228
+ form.add_field('annotation_worklist_id', worklist_id)
229
+ form.add_field('segmentation_map', json.dumps(name), content_type='application/json')
230
+
231
+ request_params = dict(
232
+ method='POST',
233
+ url=f'{self.root_url}/annotations/{resource_id}/segmentations/file',
234
+ data=form,
235
+ )
236
+ resp = await self._run_request_async(request_params)
237
+ if 'error' in resp:
238
+ raise DatamintException(resp['error'])
239
+ return resp
240
+ else:
241
+ raise ValueError(f"Volume upload not supported for file format: {file_path}")
242
+ elif isinstance(file_path, np.ndarray):
243
+ raise NotImplementedError
244
+ else:
245
+ raise ValueError(f"Unsupported file_path type for volume upload: {type(file_path)}")
246
+
247
+ _USER_LOGGER.info(f'Volume segmentation uploaded for resource {resource_id}')
248
+
249
+ async def _upload_segmentations_async(self,
250
+ resource_id: str,
251
+ frame_index: int | None,
252
+ file_path: str | np.ndarray | None = None,
253
+ fio: IO | None = None,
254
+ name: Optional[str | dict[int, str]] = None,
255
+ imported_from: Optional[str] = None,
256
+ author_email: Optional[str] = None,
257
+ discard_empty_segmentations: bool = True,
258
+ worklist_id: Optional[str] = None,
259
+ model_id: Optional[str] = None,
260
+ transpose_segmentation: bool = False,
261
+ upload_volume: bool | str = 'auto'
262
+ ) -> list[str]:
263
+ """
264
+ Upload segmentations asynchronously.
265
+
266
+ Args:
267
+ resource_id: The resource unique id.
268
+ frame_index: The frame index or None for multiple frames.
269
+ file_path: Path to segmentation file or numpy array.
270
+ fio: File-like object containing segmentation data.
271
+ name: The name of the segmentation or mapping of pixel values to names.
272
+ imported_from: The imported from value.
273
+ author_email: The author email.
274
+ discard_empty_segmentations: Whether to discard empty segmentations.
275
+ worklist_id: The annotation worklist unique id.
276
+ model_id: The model unique id.
277
+ transpose_segmentation: Whether to transpose the segmentation.
278
+ upload_volume: Whether to upload the volume as a single file or split into frames.
279
+
280
+ Returns:
281
+ List of annotation IDs created.
282
+ """
283
+ if upload_volume == 'auto':
284
+ if file_path is not None and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
285
+ upload_volume = True
286
+ else:
287
+ upload_volume = False
288
+
289
+ if file_path is not None:
290
+ # Handle volume upload
291
+ if upload_volume:
292
+ if frame_index is not None:
293
+ _LOGGER.warning("frame_index parameter ignored when upload_volume=True")
294
+
295
+ return await self._upload_volume_segmentation_async(
296
+ resource_id=resource_id,
297
+ file_path=file_path,
298
+ name=name,
299
+ imported_from=imported_from,
300
+ author_email=author_email,
301
+ worklist_id=worklist_id,
302
+ model_id=model_id,
303
+ transpose_segmentation=transpose_segmentation
304
+ )
305
+
306
+ # Handle frame-by-frame upload (existing logic)
307
+ nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(
308
+ file_path, transpose_segmentation=transpose_segmentation
309
+ )
310
+ if frame_index is None:
311
+ frame_index = list(range(nframes))
312
+
313
+ annotids = []
314
+ for fidx, f in zip(frame_index, fios):
315
+ frame_annotids = await self._upload_single_frame_segmentation_async(
316
+ resource_id=resource_id,
317
+ frame_index=fidx,
318
+ fio=f,
319
+ name=name,
320
+ imported_from=imported_from,
321
+ author_email=author_email,
322
+ discard_empty_segmentations=discard_empty_segmentations,
323
+ worklist_id=worklist_id,
324
+ model_id=model_id
325
+ )
326
+ annotids.extend(frame_annotids)
327
+ return annotids
328
+
329
+ # Handle single file-like object
330
+ if fio is not None:
331
+ if upload_volume:
332
+ raise ValueError("upload_volume=True is not supported when providing fio parameter")
333
+
334
+ return await self._upload_single_frame_segmentation_async(
335
+ resource_id=resource_id,
336
+ frame_index=frame_index,
337
+ fio=fio,
338
+ name=name,
339
+ imported_from=imported_from,
340
+ author_email=author_email,
341
+ discard_empty_segmentations=discard_empty_segmentations,
342
+ worklist_id=worklist_id,
343
+ model_id=model_id
344
+ )
345
+
346
+ raise ValueError("Either file_path or fio must be provided")
347
+
179
348
  def upload_segmentations(self,
180
349
  resource_id: str,
181
350
  file_path: str | np.ndarray,
182
351
  name: Optional[str | dict[int, str]] = None,
183
- frame_index: int | list[int] = None,
352
+ frame_index: int | list[int] | None = None,
184
353
  imported_from: Optional[str] = None,
185
354
  author_email: Optional[str] = None,
186
355
  discard_empty_segmentations: bool = True,
@@ -195,13 +364,15 @@ class AnnotationAPIHandler(BaseAPIHandler):
195
364
  resource_id (str): The resource unique id.
196
365
  file_path (str|np.ndarray): The path to the segmentation file or a numpy array.
197
366
  If a numpy array is provided, it must have the shape (height, width, #frames) or (height, width).
367
+ For NIfTI files (.nii/.nii.gz), the entire volume is uploaded as a single segmentation.
198
368
  name (Optional[Union[str, Dict[int, str]]]): The name of the segmentation or a dictionary mapping pixel values to names.
199
- example: {1: 'Femur', 2: 'Tibia'}.
369
+ example: {1: 'Femur', 2: 'Tibia'}. For NIfTI files, only string names are supported.
200
370
  frame_index (int | list[int]): The frame index of the segmentation.
201
371
  If a list, it must have the same length as the number of frames in the segmentation.
202
372
  If None, it is assumed that the segmentations are in sequential order starting from 0.
203
-
373
+ This parameter is ignored for NIfTI files as they are treated as volume segmentations.
204
374
  discard_empty_segmentations (bool): Whether to discard empty segmentations or not.
375
+ This is ignored for NIfTI files.
205
376
 
206
377
  Returns:
207
378
  str: The segmentation unique id.
@@ -211,9 +382,32 @@ class AnnotationAPIHandler(BaseAPIHandler):
211
382
 
212
383
  Example:
213
384
  >>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.png', 'SegmentationName')
385
+ >>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.nii.gz', 'VolumeSegmentation')
214
386
  """
215
387
  if isinstance(file_path, str) and not os.path.exists(file_path):
216
388
  raise FileNotFoundError(f"File {file_path} not found.")
389
+
390
+ # Handle NIfTI files specially - upload as single volume
391
+ if isinstance(file_path, str) and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
392
+ _LOGGER.info(f"Uploading NIfTI segmentation file: {file_path}")
393
+ if frame_index is not None:
394
+ raise ValueError("Do not provide frame_index for NIfTI segmentations.")
395
+ loop = asyncio.get_event_loop()
396
+ task = self._upload_segmentations_async(
397
+ resource_id=resource_id,
398
+ frame_index=None,
399
+ file_path=file_path,
400
+ name=name,
401
+ imported_from=imported_from,
402
+ author_email=author_email,
403
+ discard_empty_segmentations=False,
404
+ worklist_id=worklist_id,
405
+ model_id=model_id,
406
+ transpose_segmentation=transpose_segmentation,
407
+ upload_volume=True
408
+ )
409
+ return loop.run_until_complete(task)
410
+ # All other file types are converted to multiple PNGs and uploaded frame by frame.
217
411
  if isinstance(frame_index, int):
218
412
  frame_index = [frame_index]
219
413
 
@@ -429,7 +623,7 @@ class AnnotationAPIHandler(BaseAPIHandler):
429
623
  model_id: Optional[str] = None) -> list[str]:
430
624
  """
431
625
  Common method for creating geometry-based annotations.
432
-
626
+
433
627
  Args:
434
628
  geometry: The geometry object (LineGeometry or BoxGeometry)
435
629
  resource_id: The resource unique id
@@ -622,20 +816,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
622
816
  model_id=model_id
623
817
  )
624
818
 
625
- @deprecated(version='0.12.1', reason='Use :meth:`~get_annotations` instead with `resource_id` parameter.')
626
- def get_resource_annotations(self,
627
- resource_id: str,
628
- annotation_type: Optional[str] = None,
629
- annotator_email: Optional[str] = None,
630
- date_from: Optional[date] = None,
631
- date_to: Optional[date] = None) -> Generator[dict, None, None]:
632
-
633
- return self.get_annotations(resource_id=resource_id,
634
- annotation_type=annotation_type,
635
- annotator_email=annotator_email,
636
- date_from=date_from,
637
- date_to=date_to)
638
-
639
819
  def get_annotations(self,
640
820
  resource_id: Optional[str] = None,
641
821
  annotation_type: AnnotationType | str | None = None,