datamint 1.4.0__tar.gz → 1.5.0__tar.gz

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.

Files changed (29) hide show
  1. {datamint-1.4.0 → datamint-1.5.0}/PKG-INFO +1 -1
  2. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/annotation_api_handler.py +288 -65
  3. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/root_api_handler.py +227 -101
  4. datamint-1.5.0/datamint/client_cmd_tools/datamint_config.py +211 -0
  5. {datamint-1.4.0 → datamint-1.5.0}/datamint/client_cmd_tools/datamint_upload.py +34 -21
  6. {datamint-1.4.0 → datamint-1.5.0}/datamint/experiment/experiment.py +1 -1
  7. {datamint-1.4.0 → datamint-1.5.0}/datamint/utils/dicom_utils.py +12 -12
  8. {datamint-1.4.0 → datamint-1.5.0}/datamint/utils/io_utils.py +37 -10
  9. {datamint-1.4.0 → datamint-1.5.0}/pyproject.toml +1 -1
  10. datamint-1.4.0/datamint/client_cmd_tools/datamint_config.py +0 -168
  11. {datamint-1.4.0 → datamint-1.5.0}/README.md +0 -0
  12. {datamint-1.4.0 → datamint-1.5.0}/datamint/__init__.py +0 -0
  13. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/api_handler.py +0 -0
  14. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/base_api_handler.py +0 -0
  15. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/dto/annotation_dto.py +0 -0
  16. {datamint-1.4.0 → datamint-1.5.0}/datamint/apihandler/exp_api_handler.py +0 -0
  17. {datamint-1.4.0 → datamint-1.5.0}/datamint/client_cmd_tools/__init__.py +0 -0
  18. {datamint-1.4.0 → datamint-1.5.0}/datamint/configs.py +0 -0
  19. {datamint-1.4.0 → datamint-1.5.0}/datamint/dataset/__init__.py +0 -0
  20. {datamint-1.4.0 → datamint-1.5.0}/datamint/dataset/base_dataset.py +0 -0
  21. {datamint-1.4.0 → datamint-1.5.0}/datamint/dataset/dataset.py +0 -0
  22. {datamint-1.4.0 → datamint-1.5.0}/datamint/examples/__init__.py +0 -0
  23. {datamint-1.4.0 → datamint-1.5.0}/datamint/examples/example_projects.py +0 -0
  24. {datamint-1.4.0 → datamint-1.5.0}/datamint/experiment/__init__.py +0 -0
  25. {datamint-1.4.0 → datamint-1.5.0}/datamint/experiment/_patcher.py +0 -0
  26. {datamint-1.4.0 → datamint-1.5.0}/datamint/logging.yaml +0 -0
  27. {datamint-1.4.0 → datamint-1.5.0}/datamint/utils/logging_utils.py +0 -0
  28. {datamint-1.4.0 → datamint-1.5.0}/datamint/utils/torchmetrics.py +0 -0
  29. {datamint-1.4.0 → datamint-1.5.0}/datamint/utils/visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: datamint
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -88,41 +88,39 @@ class AnnotationAPIHandler(BaseAPIHandler):
88
88
  raise DatamintException(r['error'])
89
89
  return resp
90
90
 
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
91
+ async def _upload_single_frame_segmentation_async(self,
92
+ resource_id: str,
93
+ frame_index: int,
94
+ fio: IO,
95
+ name: Optional[str | dict[int, str]] = None,
96
+ imported_from: Optional[str] = None,
97
+ author_email: Optional[str] = None,
98
+ discard_empty_segmentations: bool = True,
99
+ worklist_id: Optional[str] = None,
100
+ model_id: Optional[str] = None
101
+ ) -> list[str]:
102
+ """
103
+ Upload a single frame segmentation asynchronously.
104
+
105
+ Args:
106
+ resource_id: The resource unique id.
107
+ frame_index: The frame index for the segmentation.
108
+ fio: File-like object containing the segmentation image.
109
+ name: The name of the segmentation or a dictionary mapping pixel values to names.
110
+ imported_from: The imported from value.
111
+ author_email: The author email.
112
+ discard_empty_segmentations: Whether to discard empty segmentations.
113
+ worklist_id: The annotation worklist unique id.
114
+ model_id: The model unique id.
115
+
116
+ Returns:
117
+ List of annotation IDs created.
118
+ """
122
119
  try:
123
120
  try:
124
121
  img = np.array(Image.open(fio))
125
- ### Check that frame is not empty ###
122
+
123
+ # Check that frame is not empty
126
124
  uniq_vals = np.unique(img)
127
125
  if discard_empty_segmentations:
128
126
  if len(uniq_vals) == 1 and uniq_vals[0] == 0:
@@ -135,31 +133,38 @@ class AnnotationAPIHandler(BaseAPIHandler):
135
133
 
136
134
  segnames = AnnotationAPIHandler._get_segmentation_names(uniq_vals, names=name)
137
135
  segs_generator = AnnotationAPIHandler._split_segmentations(img, uniq_vals, fio)
136
+
137
+ # Create annotations
138
138
  annotations: list[CreateAnnotationDto] = []
139
139
  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)
140
+ ann = CreateAnnotationDto(
141
+ type='segmentation',
142
+ identifier=segname,
143
+ scope='frame',
144
+ frame_index=frame_index,
145
+ imported_from=imported_from,
146
+ import_author=author_email,
147
+ model_id=model_id,
148
+ annotation_worklist_id=worklist_id
149
+ )
148
150
  annotations.append(ann)
149
- # raise ValueError if there is multiple annotations with the same identifier, frame_index, scope and author
151
+
152
+ # Validate unique identifiers
150
153
  if len(annotations) != len(set([a.identifier for a in annotations])):
151
154
  raise ValueError(
152
- "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet.")
155
+ "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet."
156
+ )
153
157
 
154
158
  annotids = await self._upload_annotations_async(resource_id, annotations)
155
159
 
156
- ### Upload segmentation ###
160
+ # Upload segmentation files
157
161
  if len(annotids) != len(segnames):
158
162
  _LOGGER.warning(f"Number of uploaded annotations ({len(annotids)})" +
159
163
  f" does not match the number of annotations ({len(segnames)})")
160
- for annotid, segname, fio in zip(annotids, segnames, segs_generator):
164
+
165
+ for annotid, segname, fio_seg in zip(annotids, segnames, segs_generator):
161
166
  form = aiohttp.FormData()
162
- form.add_field('file', fio, filename=segname, content_type='image/png')
167
+ form.add_field('file', fio_seg, filename=segname, content_type='image/png')
163
168
  request_params = dict(
164
169
  method='POST',
165
170
  url=f'{self.root_url}/annotations/{resource_id}/annotations/{annotid}/file',
@@ -168,19 +173,226 @@ class AnnotationAPIHandler(BaseAPIHandler):
168
173
  resp = await self._run_request_async(request_params)
169
174
  if 'error' in resp:
170
175
  raise DatamintException(resp['error'])
171
- #######
176
+
177
+ return annotids
172
178
  finally:
173
179
  fio.close()
174
- _USER_LOGGER.info(f'Segmentations uploaded for resource {resource_id}')
175
- return annotids
176
180
  except ResourceNotFoundError:
177
181
  raise ResourceNotFoundError('resource', {'resource_id': resource_id})
178
182
 
183
+ async def _upload_volume_segmentation_async(self,
184
+ resource_id: str,
185
+ file_path: str | np.ndarray,
186
+ name: Optional[str] = None,
187
+ imported_from: Optional[str] = None,
188
+ author_email: Optional[str] = None,
189
+ worklist_id: Optional[str] = None,
190
+ model_id: Optional[str] = None,
191
+ transpose_segmentation: bool = False
192
+ ) -> list[str]:
193
+ """
194
+ Upload a volume segmentation as a single file asynchronously.
195
+
196
+ Args:
197
+ resource_id: The resource unique id.
198
+ file_path: Path to segmentation file or numpy array.
199
+ name: The name of the segmentation (string only for volumes).
200
+ imported_from: The imported from value.
201
+ author_email: The author email.
202
+ worklist_id: The annotation worklist unique id.
203
+ model_id: The model unique id.
204
+ transpose_segmentation: Whether to transpose the segmentation.
205
+
206
+ Returns:
207
+ List of annotation IDs created.
208
+
209
+ Raises:
210
+ ValueError: If name is not a string or file format is unsupported for volume upload.
211
+ """
212
+ if isinstance(name, dict):
213
+ raise ValueError("Volume uploads only support string names, not dictionaries.")
214
+
215
+ if name is None:
216
+ name = 'volume_segmentation'
217
+
218
+ # Create volume annotation
219
+ ann = CreateAnnotationDto(
220
+ type='segmentation',
221
+ identifier=name,
222
+ scope='frame', # Volume segmentations use image scope
223
+ imported_from=imported_from,
224
+ import_author=author_email,
225
+ model_id=model_id,
226
+ annotation_worklist_id=worklist_id
227
+ )
228
+
229
+ annotids = await self._upload_annotations_async(resource_id, [ann])
230
+ _LOGGER.debug(f"Created volume annotation with ID: {annotids}")
231
+
232
+ if not annotids:
233
+ raise DatamintException("Failed to create volume annotation")
234
+
235
+ annotid = annotids[0]
236
+
237
+ # Prepare file for upload
238
+ if isinstance(file_path, str):
239
+ if file_path.endswith('.nii') or file_path.endswith('.nii.gz'):
240
+ content_type = 'application/x-nifti'
241
+ # Upload NIfTI file directly
242
+ with open(file_path, 'rb') as f:
243
+ filename = os.path.basename(file_path)
244
+ form = aiohttp.FormData()
245
+ form.add_field('file', f, filename=filename, content_type='application/x-nifti')
246
+
247
+ request_params = dict(
248
+ method='POST',
249
+ url=f'{self.root_url}/annotations/{resource_id}/annotations/{annotid}/file',
250
+ data=form,
251
+ )
252
+ resp = await self._run_request_async(request_params)
253
+ if 'error' in resp:
254
+ raise DatamintException(resp['error'])
255
+ else:
256
+ raise ValueError(f"Volume upload not supported for file format: {file_path}")
257
+ elif isinstance(file_path, np.ndarray):
258
+ # Convert numpy array to NIfTI and upload
259
+ # TODO: Consider supporting direct numpy array upload or convert to a supported format
260
+ if transpose_segmentation:
261
+ volume_data = file_path.transpose(1, 0, 2) if file_path.ndim == 3 else file_path.transpose(1, 0)
262
+ else:
263
+ volume_data = file_path
264
+
265
+ # Create temporary NIfTI file
266
+ import tempfile
267
+ with tempfile.NamedTemporaryFile(suffix='.nii.gz', delete=False) as tmp_file:
268
+ nii_img = nib.Nifti1Image(volume_data.astype(np.uint8), np.eye(4))
269
+ nib.save(nii_img, tmp_file.name)
270
+
271
+ try:
272
+ with open(tmp_file.name, 'rb') as f:
273
+ form = aiohttp.FormData()
274
+ form.add_field('file', f, filename=f'{name}.nii.gz', content_type='application/x-nifti')
275
+
276
+ request_params = dict(
277
+ method='POST',
278
+ url=f'{self.root_url}/annotations/{resource_id}/annotations/{annotid}/file',
279
+ data=form,
280
+ )
281
+ resp = await self._run_request_async(request_params)
282
+ if 'error' in resp:
283
+ raise DatamintException(resp['error'])
284
+ finally:
285
+ os.unlink(tmp_file.name) # Clean up temporary file
286
+ else:
287
+ raise ValueError(f"Unsupported file_path type for volume upload: {type(file_path)}")
288
+
289
+ _USER_LOGGER.info(f'Volume segmentation uploaded for resource {resource_id}')
290
+ return annotids
291
+
292
+ async def _upload_segmentations_async(self,
293
+ resource_id: str,
294
+ frame_index: int | None,
295
+ file_path: str | np.ndarray | None = None,
296
+ fio: IO | None = None,
297
+ name: Optional[str | dict[int, str]] = None,
298
+ imported_from: Optional[str] = None,
299
+ author_email: Optional[str] = None,
300
+ discard_empty_segmentations: bool = True,
301
+ worklist_id: Optional[str] = None,
302
+ model_id: Optional[str] = None,
303
+ transpose_segmentation: bool = False,
304
+ upload_volume: bool | str = 'auto'
305
+ ) -> list[str]:
306
+ """
307
+ Upload segmentations asynchronously.
308
+
309
+ Args:
310
+ resource_id: The resource unique id.
311
+ frame_index: The frame index or None for multiple frames.
312
+ file_path: Path to segmentation file or numpy array.
313
+ fio: File-like object containing segmentation data.
314
+ name: The name of the segmentation or mapping of pixel values to names.
315
+ imported_from: The imported from value.
316
+ author_email: The author email.
317
+ discard_empty_segmentations: Whether to discard empty segmentations.
318
+ worklist_id: The annotation worklist unique id.
319
+ model_id: The model unique id.
320
+ transpose_segmentation: Whether to transpose the segmentation.
321
+ upload_volume: Whether to upload the volume as a single file or split into frames.
322
+
323
+ Returns:
324
+ List of annotation IDs created.
325
+ """
326
+ if upload_volume == 'auto':
327
+ if file_path is not None and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
328
+ upload_volume = True
329
+ else:
330
+ upload_volume = False
331
+
332
+ if file_path is not None:
333
+ # Handle volume upload
334
+ if upload_volume:
335
+ if frame_index is not None:
336
+ _LOGGER.warning("frame_index parameter ignored when upload_volume=True")
337
+
338
+ return await self._upload_volume_segmentation_async(
339
+ resource_id=resource_id,
340
+ file_path=file_path,
341
+ name=name if isinstance(name, str) else None,
342
+ imported_from=imported_from,
343
+ author_email=author_email,
344
+ worklist_id=worklist_id,
345
+ model_id=model_id,
346
+ transpose_segmentation=transpose_segmentation
347
+ )
348
+
349
+ # Handle frame-by-frame upload (existing logic)
350
+ nframes, fios = AnnotationAPIHandler._generate_segmentations_ios(
351
+ file_path, transpose_segmentation=transpose_segmentation
352
+ )
353
+ if frame_index is None:
354
+ frame_index = list(range(nframes))
355
+
356
+ annotids = []
357
+ for fidx, f in zip(frame_index, fios):
358
+ frame_annotids = await self._upload_single_frame_segmentation_async(
359
+ resource_id=resource_id,
360
+ frame_index=fidx,
361
+ fio=f,
362
+ name=name,
363
+ imported_from=imported_from,
364
+ author_email=author_email,
365
+ discard_empty_segmentations=discard_empty_segmentations,
366
+ worklist_id=worklist_id,
367
+ model_id=model_id
368
+ )
369
+ annotids.extend(frame_annotids)
370
+ return annotids
371
+
372
+ # Handle single file-like object
373
+ if fio is not None:
374
+ if upload_volume:
375
+ raise ValueError("upload_volume=True is not supported when providing fio parameter")
376
+
377
+ return await self._upload_single_frame_segmentation_async(
378
+ resource_id=resource_id,
379
+ frame_index=frame_index,
380
+ fio=fio,
381
+ name=name,
382
+ imported_from=imported_from,
383
+ author_email=author_email,
384
+ discard_empty_segmentations=discard_empty_segmentations,
385
+ worklist_id=worklist_id,
386
+ model_id=model_id
387
+ )
388
+
389
+ raise ValueError("Either file_path or fio must be provided")
390
+
179
391
  def upload_segmentations(self,
180
392
  resource_id: str,
181
393
  file_path: str | np.ndarray,
182
394
  name: Optional[str | dict[int, str]] = None,
183
- frame_index: int | list[int] = None,
395
+ frame_index: int | list[int] | None = None,
184
396
  imported_from: Optional[str] = None,
185
397
  author_email: Optional[str] = None,
186
398
  discard_empty_segmentations: bool = True,
@@ -195,13 +407,15 @@ class AnnotationAPIHandler(BaseAPIHandler):
195
407
  resource_id (str): The resource unique id.
196
408
  file_path (str|np.ndarray): The path to the segmentation file or a numpy array.
197
409
  If a numpy array is provided, it must have the shape (height, width, #frames) or (height, width).
410
+ For NIfTI files (.nii/.nii.gz), the entire volume is uploaded as a single segmentation.
198
411
  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'}.
412
+ example: {1: 'Femur', 2: 'Tibia'}. For NIfTI files, only string names are supported.
200
413
  frame_index (int | list[int]): The frame index of the segmentation.
201
414
  If a list, it must have the same length as the number of frames in the segmentation.
202
415
  If None, it is assumed that the segmentations are in sequential order starting from 0.
203
-
416
+ This parameter is ignored for NIfTI files as they are treated as volume segmentations.
204
417
  discard_empty_segmentations (bool): Whether to discard empty segmentations or not.
418
+ This is ignored for NIfTI files.
205
419
 
206
420
  Returns:
207
421
  str: The segmentation unique id.
@@ -211,9 +425,32 @@ class AnnotationAPIHandler(BaseAPIHandler):
211
425
 
212
426
  Example:
213
427
  >>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.png', 'SegmentationName')
428
+ >>> api_handler.upload_segmentation(resource_id, 'path/to/segmentation.nii.gz', 'VolumeSegmentation')
214
429
  """
215
430
  if isinstance(file_path, str) and not os.path.exists(file_path):
216
431
  raise FileNotFoundError(f"File {file_path} not found.")
432
+
433
+ # Handle NIfTI files specially - upload as single volume
434
+ if isinstance(file_path, str) and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
435
+ _LOGGER.info(f"Uploading NIfTI segmentation file: {file_path}")
436
+ if frame_index is not None:
437
+ raise ValueError("Do not provide frame_index for NIfTI segmentations.")
438
+ loop = asyncio.get_event_loop()
439
+ task = self._upload_segmentations_async(
440
+ resource_id=resource_id,
441
+ frame_index=None,
442
+ file_path=file_path,
443
+ name=name,
444
+ imported_from=imported_from,
445
+ author_email=author_email,
446
+ discard_empty_segmentations=False,
447
+ worklist_id=worklist_id,
448
+ model_id=model_id,
449
+ transpose_segmentation=transpose_segmentation,
450
+ upload_volume=True
451
+ )
452
+ return loop.run_until_complete(task)
453
+ # All other file types are converted to multiple PNGs and uploaded frame by frame.
217
454
  if isinstance(frame_index, int):
218
455
  frame_index = [frame_index]
219
456
 
@@ -622,20 +859,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
622
859
  model_id=model_id
623
860
  )
624
861
 
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
862
  def get_annotations(self,
640
863
  resource_id: Optional[str] = None,
641
864
  annotation_type: AnnotationType | str | None = None,