datamint 2.44.0__tar.gz → 2.45.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.
Files changed (191) hide show
  1. {datamint-2.44.0 → datamint-2.45.0}/PKG-INFO +2 -2
  2. {datamint-2.44.0 → datamint-2.45.0}/README.md +1 -1
  3. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/annotations_api.py +93 -105
  4. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/annotationsets_api.py +8 -6
  5. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/projects_api.py +8 -6
  6. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/resources_api.py +3 -14
  7. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/base_segmentation.py +58 -1
  8. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/model.py +26 -19
  9. datamint-2.45.0/datamint/utils/date_utils.py +21 -0
  10. {datamint-2.44.0 → datamint-2.45.0}/pyproject.toml +1 -1
  11. {datamint-2.44.0 → datamint-2.45.0}/datamint/__init__.py +0 -0
  12. {datamint-2.44.0 → datamint-2.45.0}/datamint/__main__.py +0 -0
  13. {datamint-2.44.0 → datamint-2.45.0}/datamint/_repr_utils.py +0 -0
  14. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/__init__.py +0 -0
  15. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/base_api.py +0 -0
  16. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/client.py +0 -0
  17. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/dto/__init__.py +0 -0
  18. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/dto/annotation_dto.py +0 -0
  19. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/dto/inference_prompts.py +0 -0
  20. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/dto/save_results_options.py +0 -0
  21. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/__init__.py +0 -0
  22. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/datasetsinfo_api.py +0 -0
  23. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/deploy_model_api.py +0 -0
  24. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/inference_api.py +0 -0
  25. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/model_types.py +0 -0
  26. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/models_api.py +0 -0
  27. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/pod_logs_api.py +0 -0
  28. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/users_api.py +0 -0
  29. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/endpoints/validation_studies_api.py +0 -0
  30. {datamint-2.44.0 → datamint-2.45.0}/datamint/api/entity_base_api.py +0 -0
  31. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/__init__.py +0 -0
  32. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_config.py +0 -0
  33. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_download.py +0 -0
  34. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_example.py +0 -0
  35. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_import.py +0 -0
  36. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_inference.py +0 -0
  37. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_init.py +0 -0
  38. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_train.py +0 -0
  39. {datamint-2.44.0 → datamint-2.45.0}/datamint/client_cmd_tools/datamint_upload.py +0 -0
  40. {datamint-2.44.0 → datamint-2.45.0}/datamint/configs.py +0 -0
  41. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/__init__.py +0 -0
  42. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/annotation_processor.py +0 -0
  43. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/base.py +0 -0
  44. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/factory.py +0 -0
  45. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/image_dataset.py +0 -0
  46. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/multiframe_dataset.py +0 -0
  47. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/sliced_dataset.py +0 -0
  48. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/sliced_video_dataset.py +0 -0
  49. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/split_result.py +0 -0
  50. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/video_dataset.py +0 -0
  51. {datamint-2.44.0 → datamint-2.45.0}/datamint/dataset/volume_dataset.py +0 -0
  52. {datamint-2.44.0 → datamint-2.45.0}/datamint/default_project.py +0 -0
  53. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/__init__.py +0 -0
  54. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotation_worklist.py +0 -0
  55. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/__init__.py +0 -0
  56. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/annotation.py +0 -0
  57. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/annotation_spec.py +0 -0
  58. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/base_geometry.py +0 -0
  59. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/box_annotation.py +0 -0
  60. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/category_annotation.py +0 -0
  61. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/geometry.py +0 -0
  62. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/image_classification.py +0 -0
  63. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/image_segmentation.py +0 -0
  64. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/label_annotation.py +0 -0
  65. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/line_annotation.py +0 -0
  66. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/numeric_annotation.py +0 -0
  67. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/point_annotation.py +0 -0
  68. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/region_annotation.py +0 -0
  69. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/text_annotation.py +0 -0
  70. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/types.py +0 -0
  71. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/annotations/volume_segmentation.py +0 -0
  72. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/base_entity.py +0 -0
  73. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/buildlog.py +0 -0
  74. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/cache_manager.py +0 -0
  75. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/channel.py +0 -0
  76. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/datasetinfo.py +0 -0
  77. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/deployjob.py +0 -0
  78. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/inferencejob.py +0 -0
  79. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/pod.py +0 -0
  80. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/project.py +0 -0
  81. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/project_resource_split.py +0 -0
  82. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resource.py +0 -0
  83. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/__init__.py +0 -0
  84. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/dicom_resource.py +0 -0
  85. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/image_resource.py +0 -0
  86. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/nifti_resource.py +0 -0
  87. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/video_resource.py +0 -0
  88. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/resources/volume_resource.py +0 -0
  89. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/sliced_resource.py +0 -0
  90. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/sliced_resource_base.py +0 -0
  91. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/sliced_video_resource.py +0 -0
  92. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/user.py +0 -0
  93. {datamint-2.44.0 → datamint-2.45.0}/datamint/entities/validation_study.py +0 -0
  94. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/__init__.py +0 -0
  95. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/evaluator.py +0 -0
  96. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/metrics/__init__.py +0 -0
  97. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/metrics/classification.py +0 -0
  98. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/metrics/detection.py +0 -0
  99. {datamint-2.44.0 → datamint-2.45.0}/datamint/evaluate/metrics/segmentation.py +0 -0
  100. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/__init__.py +0 -0
  101. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/_common.py +0 -0
  102. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/_download.py +0 -0
  103. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/bccd_dataset.py +0 -0
  104. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/busi_dataset.py +0 -0
  105. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/example_projects.py +0 -0
  106. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/fracatlas_dataset.py +0 -0
  107. {datamint-2.44.0 → datamint-2.45.0}/datamint/examples/synapse_dataset.py +0 -0
  108. {datamint-2.44.0 → datamint-2.45.0}/datamint/exceptions.py +0 -0
  109. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/__init__.py +0 -0
  110. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/_common.py +0 -0
  111. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/_detect.py +0 -0
  112. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/coco.py +0 -0
  113. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/pascal_voc.py +0 -0
  114. {datamint-2.44.0 → datamint-2.45.0}/datamint/importers/yolo.py +0 -0
  115. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/__init__.py +0 -0
  116. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/benchmark.py +0 -0
  117. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/datamodule.py +0 -0
  118. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/__init__.py +0 -0
  119. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/base_trainer.py +0 -0
  120. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/classification_trainer.py +0 -0
  121. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/detection_trainer.py +0 -0
  122. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/__init__.py +0 -0
  123. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/base.py +0 -0
  124. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/classification_module.py +0 -0
  125. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/detection_modules/__init__.py +0 -0
  126. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/detection_modules/yolox_module.py +0 -0
  127. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_module.py +0 -0
  128. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/__init__.py +0 -0
  129. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/deeplabv3plus.py +0 -0
  130. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/smp_module.py +0 -0
  131. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/transunet.py +0 -0
  132. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/unetpp.py +0 -0
  133. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/unetrpp.py +0 -0
  134. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/seg2d_trainer.py +0 -0
  135. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/seg3d_trainer.py +0 -0
  136. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/segmentation_trainer.py +0 -0
  137. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/__init__.py +0 -0
  138. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/deeplabv3plus.py +0 -0
  139. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/efficientnetv2.py +0 -0
  140. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/__init__.py +0 -0
  141. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/_nnunet_trainer_bridge.py +0 -0
  142. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/data_export.py +0 -0
  143. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/data_import.py +0 -0
  144. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/inference_model.py +0 -0
  145. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/nnunet/trainer.py +0 -0
  146. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/transunet.py +0 -0
  147. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/unetpp.py +0 -0
  148. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/unetrpp.py +0 -0
  149. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/specialized/yolox.py +0 -0
  150. {datamint-2.44.0 → datamint-2.45.0}/datamint/lightning/trainers/vol_seg_trainer.py +0 -0
  151. {datamint-2.44.0 → datamint-2.45.0}/datamint/logging.yaml +0 -0
  152. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/__init__.py +0 -0
  153. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/artifact/__init__.py +0 -0
  154. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/artifact/datamint_artifacts_repo.py +0 -0
  155. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/data/__init__.py +0 -0
  156. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/data/datamint_dataset.py +0 -0
  157. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/env_utils.py +0 -0
  158. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/env_vars.py +0 -0
  159. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/__init__.py +0 -0
  160. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/annotation_specs.py +0 -0
  161. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/datamint_flavor.py +0 -0
  162. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/model_loader.py +0 -0
  163. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/model_parser.py +0 -0
  164. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/prediction_modes.py +0 -0
  165. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/prediction_router.py +0 -0
  166. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/task_type.py +0 -0
  167. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/flavors/validation.py +0 -0
  168. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/lightning/callbacks/__init__.py +0 -0
  169. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/lightning/callbacks/modelcheckpoint.py +0 -0
  170. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/Dockerfile.template +0 -0
  171. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/__init__.py +0 -0
  172. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/datamint_model_store.py +0 -0
  173. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/docker_build.py +0 -0
  174. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/local_inference.py +0 -0
  175. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/serve.py +0 -0
  176. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/models/tags.py +0 -0
  177. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/store_utils.py +0 -0
  178. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/tracking/datamint_store.py +0 -0
  179. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/tracking/default_experiment.py +0 -0
  180. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/tracking/fluent.py +0 -0
  181. {datamint-2.44.0 → datamint-2.45.0}/datamint/mlflow/tracking/offline_buffer.py +0 -0
  182. {datamint-2.44.0 → datamint-2.45.0}/datamint/types.py +0 -0
  183. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/annotation_agreement.py +0 -0
  184. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/collection_utils.py +0 -0
  185. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/env.py +0 -0
  186. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/logging_utils.py +0 -0
  187. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/nifti_utils.py +0 -0
  188. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/torchmetrics.py +0 -0
  189. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/uncertainty.py +0 -0
  190. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/urls.py +0 -0
  191. {datamint-2.44.0 → datamint-2.45.0}/datamint/utils/visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamint
3
- Version: 2.44.0
3
+ Version: 2.45.0
4
4
  Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
5
5
  Keywords: medical-imaging,dicom,nifti,pytorch,mlflow,lightning,datasets,annotation
6
6
  Author: SonanceAI
@@ -84,7 +84,7 @@ Description-Content-Type: text/markdown
84
84
  ![Build Status](https://github.com/SonanceAI/datamint-python-api/actions/workflows/run_test.yaml/badge.svg)
85
85
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
86
86
 
87
- Datamint turns medical imaging ML work. Dataset management, annotation, training, and deployment into a few lines of Python, with built-in support for DICOM/NIfTI/PNG, PyTorch Lightning trainers, and MLflow tracking.
87
+ Datamint turns medical imaging ML work (dataset management, annotation, training and deployment) into a few lines of Python, with built-in support for DICOM, NIfTI and PNG, PyTorch Lightning trainers and MLflow tracking.
88
88
 
89
89
  **Common use cases:** 🩻 Segmentation · 🏷️ Classification · 📦 Detection
90
90
 
@@ -7,7 +7,7 @@
7
7
  ![Build Status](https://github.com/SonanceAI/datamint-python-api/actions/workflows/run_test.yaml/badge.svg)
8
8
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
9
9
 
10
- Datamint turns medical imaging ML work. Dataset management, annotation, training, and deployment into a few lines of Python, with built-in support for DICOM/NIfTI/PNG, PyTorch Lightning trainers, and MLflow tracking.
10
+ Datamint turns medical imaging ML work (dataset management, annotation, training and deployment) into a few lines of Python, with built-in support for DICOM, NIfTI and PNG, PyTorch Lightning trainers and MLflow tracking.
11
11
 
12
12
  **Common use cases:** 🩻 Segmentation · 🏷️ Classification · 📦 Detection
13
13
 
@@ -6,7 +6,7 @@ from collections.abc import Generator, Sequence
6
6
  from datetime import date
7
7
  from io import BytesIO
8
8
  from pathlib import Path
9
- from typing import IO, TYPE_CHECKING, Any, BinaryIO, Literal, overload
9
+ from typing import IO, TYPE_CHECKING, Any, Literal, overload
10
10
 
11
11
  import aiohttp
12
12
  import httpx
@@ -36,6 +36,7 @@ from datamint.entities.annotations import (
36
36
  annotation_from_dict,
37
37
  )
38
38
  from datamint.exceptions import ItemNotFoundError, ServerError
39
+ from datamint.utils.date_utils import to_iso_date_str
39
40
  from datamint.utils.nifti_utils import metadata_to_nifti_obj
40
41
 
41
42
  from ..entity_base_api import ApiConfig, CreatableEntityApi, DeletableEntityApi
@@ -81,8 +82,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
81
82
  resource: str | Resource | Sequence[str | Resource] | None = None,
82
83
  annotation_type: AnnotationType | str | None = None,
83
84
  annotator_email: str | None = None,
84
- from_date: date | None = None,
85
- to_date: date | None = None,
85
+ from_date: date | str | None = None,
86
+ to_date: date | str | None = None,
86
87
  dataset_id: str | None = None,
87
88
  worklist_id: str | None = None,
88
89
  status: Literal['new', 'published'] | None = None,
@@ -98,8 +99,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
98
99
  resource: str | Resource | Sequence[str | Resource] | None = None,
99
100
  annotation_type: AnnotationType | str | None = None,
100
101
  annotator_email: str | None = None,
101
- from_date: date | None = None,
102
- to_date: date | None = None,
102
+ from_date: date | str | None = None,
103
+ to_date: date | str | None = None,
103
104
  dataset_id: str | None = None,
104
105
  worklist_id: str | None = None,
105
106
  status: Literal['new', 'published'] | None = None,
@@ -116,8 +117,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
116
117
  resource: str | Resource | Sequence[str | Resource] | None = None,
117
118
  annotation_type: AnnotationType | str | None = None,
118
119
  annotator_email: str | None = None,
119
- from_date: date | None = None,
120
- to_date: date | None = None,
120
+ from_date: date | str | None = None,
121
+ to_date: date | str | None = None,
121
122
  dataset_id: str | None = None,
122
123
  worklist_id: str | None = None,
123
124
  status: Literal['new', 'published'] | None = None,
@@ -136,8 +137,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
136
137
  a list of resources, or None to retrieve annotations from all resources.
137
138
  annotation_type: Filter by annotation type (e.g., 'segmentation', 'category').
138
139
  annotator_email: Filter by annotator email address.
139
- from_date: Filter annotations created on or after this date.
140
- to_date: Filter annotations created on or before this date.
140
+ from_date: Filter annotations created on or after this date (``date`` or ISO date string).
141
+ to_date: Filter annotations created on or before this date (``date`` or ISO date string).
141
142
  dataset_id: Filter by dataset unique id.
142
143
  worklist_id: Filter by annotation worklist unique id.
143
144
  status: Filter by annotation status ('new' or 'published').
@@ -190,8 +191,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
190
191
  payload = {
191
192
  'annotation_type': annotation_type,
192
193
  'annotatorEmail': annotator_email,
193
- 'from': from_date.isoformat() if from_date is not None else None,
194
- 'to': to_date.isoformat() if to_date is not None else None,
194
+ 'from': to_iso_date_str(from_date),
195
+ 'to': to_iso_date_str(to_date),
195
196
  'dataset_id': dataset_id,
196
197
  'annotation_worklist_id': worklist_id,
197
198
  'status': status,
@@ -307,7 +308,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
307
308
  )
308
309
 
309
310
  # Handle frame-by-frame upload (existing logic)
310
- nframes, fios = AnnotationsApi._generate_segmentations_ios(
311
+ nframes, frames = AnnotationsApi._generate_segmentation_frames(
311
312
  file_path, transpose_segmentation=transpose_segmentation
312
313
  )
313
314
  if frame_index is None:
@@ -322,11 +323,11 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
322
323
  raise ValueError("frame_index must be a list of integers or None.")
323
324
 
324
325
  annotids = []
325
- for fidx, f in zip(frames_indices, fios):
326
+ for fidx, frame in zip(frames_indices, frames):
326
327
  frame_annotids = await self._upload_single_frame_segmentation_async(
327
328
  resource_id=resource_id,
328
329
  frame_index=fidx,
329
- fio=f,
330
+ frame=frame,
330
331
  name=name,
331
332
  imported_from=imported_from,
332
333
  author_email=author_email,
@@ -342,7 +343,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
342
343
  async def _upload_single_frame_segmentation_async(self,
343
344
  resource_id: str,
344
345
  frame_index: int | None,
345
- fio: IO,
346
+ frame: np.ndarray,
346
347
  name: dict[int, str] | dict[tuple, str],
347
348
  imported_from: str | None = None,
348
349
  author_email: str | None = None,
@@ -358,7 +359,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
358
359
  Args:
359
360
  resource_id: The resource unique id.
360
361
  frame_index: The frame index for the segmentation.
361
- fio: File-like object containing the segmentation image.
362
+ frame: Segmentation frame as a uint8 array of shape (height, width, 3).
362
363
  name: The name of the segmentation, a dictionary mapping pixel values to names,
363
364
  or a dictionary mapping RGB tuples to names.
364
365
  imported_from: The imported from value.
@@ -372,81 +373,73 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
372
373
  List of annotation IDs created.
373
374
  """
374
375
  try:
375
- try:
376
- img_pil = Image.open(fio)
377
- img_array = np.array(img_pil) # shape: (height, width, channels)
378
- # Returns a list of (count, color) tuples
379
- unique_vals = img_pil.getcolors(maxcolors=MAX_NUMBER_DISTINCT_COLORS)
380
- # convert to list of RGB tuples
381
- if unique_vals is None:
382
- raise ValueError(f'Number of unique colors exceeds {MAX_NUMBER_DISTINCT_COLORS}.')
383
- rgb_unique_vals = [
384
- (int(color[0]), int(color[1]), int(color[2]))
385
- for _, color in unique_vals
386
- if isinstance(color, tuple) and len(color) >= 3
387
- ]
388
- # Remove black/transparent pixels
389
- black_pixel = (0, 0, 0)
390
- rgb_unique_vals = [rgb for rgb in rgb_unique_vals if rgb != black_pixel]
391
-
392
- rgb_name_map: dict[tuple[int, int, int] | str, str] = {}
393
- for key, value in name.items():
394
- if key == 'default':
395
- rgb_name_map['default'] = value
396
- elif isinstance(key, tuple) and len(key) >= 3:
397
- rgb_name_map[(int(key[0]), int(key[1]), int(key[2]))] = value
398
-
399
- if discard_empty_segmentations:
400
- if len(rgb_unique_vals) == 0:
401
- msg = f"Discarding empty RGB segmentation for frame {frame_index}"
402
- _LOGGER.debug(msg)
403
- _USER_LOGGER.debug(msg)
404
- return []
405
- segnames = AnnotationsApi._get_segmentation_names_rgb(rgb_unique_vals, names=rgb_name_map)
406
- segs_generator = AnnotationsApi._split_rgb_segmentations(img_array, rgb_unique_vals)
407
-
408
- fio.seek(0)
409
- # TODO: Optimize this. It is not necessary to open the image twice.
410
-
411
- # Create annotations
412
- annotations: list[CreateAnnotationDto] = []
413
- for segname in segnames:
414
- ann = CreateAnnotationDto(
415
- type='segmentation',
416
- identifier=segname,
417
- scope='frame',
418
- frame_index=frame_index,
419
- imported_from=imported_from,
420
- import_author=author_email,
421
- model_id=model_id,
422
- annotation_worklist_id=worklist_id,
423
- source=source,
424
- )
425
- annotations.append(ann)
426
-
427
- # Validate unique identifiers
428
- if len(annotations) != len({a.identifier for a in annotations}):
429
- raise ValueError(
430
- "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet."
431
- )
432
-
433
- annotids = await self._create_annotations_async(resource_id=resource_id,
434
- annotations_dto=annotations,
435
- session=session)
376
+ # Returns a list of (count, color) tuples
377
+ unique_vals = Image.fromarray(frame).getcolors(maxcolors=MAX_NUMBER_DISTINCT_COLORS)
378
+ # convert to list of RGB tuples
379
+ if unique_vals is None:
380
+ raise ValueError(f'Number of unique colors exceeds {MAX_NUMBER_DISTINCT_COLORS}.')
381
+ rgb_unique_vals = [
382
+ (int(color[0]), int(color[1]), int(color[2]))
383
+ for _, color in unique_vals
384
+ if isinstance(color, tuple) and len(color) >= 3
385
+ ]
386
+ # Remove black pixels
387
+ black_pixel = (0, 0, 0)
388
+ rgb_unique_vals = [rgb for rgb in rgb_unique_vals if rgb != black_pixel]
389
+
390
+ rgb_name_map: dict[tuple[int, int, int] | str, str] = {}
391
+ for key, value in name.items():
392
+ if key == 'default':
393
+ rgb_name_map['default'] = value
394
+ elif isinstance(key, tuple) and len(key) >= 3:
395
+ rgb_name_map[(int(key[0]), int(key[1]), int(key[2]))] = value
396
+
397
+ if discard_empty_segmentations:
398
+ if len(rgb_unique_vals) == 0:
399
+ msg = f"Discarding empty RGB segmentation for frame {frame_index}"
400
+ _LOGGER.debug(msg)
401
+ _USER_LOGGER.debug(msg)
402
+ return []
403
+ segnames = AnnotationsApi._get_segmentation_names_rgb(rgb_unique_vals, names=rgb_name_map)
404
+ segs_generator = AnnotationsApi._split_rgb_segmentations(frame, rgb_unique_vals)
405
+
406
+ # Create annotations
407
+ annotations: list[CreateAnnotationDto] = []
408
+ for segname in segnames:
409
+ ann = CreateAnnotationDto(
410
+ type='segmentation',
411
+ identifier=segname,
412
+ scope='frame',
413
+ frame_index=frame_index,
414
+ imported_from=imported_from,
415
+ import_author=author_email,
416
+ model_id=model_id,
417
+ annotation_worklist_id=worklist_id,
418
+ source=source,
419
+ )
420
+ annotations.append(ann)
436
421
 
437
- # Upload segmentation files
438
- if len(annotids) != len(segnames):
439
- _LOGGER.warning(f"Number of uploaded annotations ({len(annotids)})" +
440
- f" does not match the number of annotations ({len(segnames)})")
422
+ # Validate unique identifiers
423
+ if len(annotations) != len({a.identifier for a in annotations}):
424
+ raise ValueError(
425
+ "Multiple annotations with the same identifier, frame_index, scope and author is not supported yet."
426
+ )
441
427
 
442
- for annotid, segname, fio_seg in zip(annotids, segnames, segs_generator):
443
- await self.upload_annotation_file_async(resource_id, annotid, fio_seg,
444
- content_type='image/png',
445
- filename=segname,
428
+ annotids = await self._create_annotations_async(resource_id=resource_id,
429
+ annotations_dto=annotations,
446
430
  session=session)
447
- return annotids
448
- finally:
449
- fio.close()
431
+
432
+ # Upload segmentation files
433
+ if len(annotids) != len(segnames):
434
+ _LOGGER.warning(f"Number of uploaded annotations ({len(annotids)})" +
435
+ f" does not match the number of annotations ({len(segnames)})")
436
+
437
+ for annotid, segname, fio_seg in zip(annotids, segnames, segs_generator):
438
+ await self.upload_annotation_file_async(resource_id, annotid, fio_seg,
439
+ content_type='image/png',
440
+ filename=segname,
441
+ session=session)
442
+ return annotids
450
443
  except ItemNotFoundError:
451
444
  raise ItemNotFoundError('resource', {'resource_id': resource_id})
452
445
 
@@ -1013,9 +1006,9 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1013
1006
  _USER_LOGGER.info(f'Volume segmentation uploaded for resource {resource_id}')
1014
1007
 
1015
1008
  @staticmethod
1016
- def _generate_segmentations_ios(file_path: str | np.ndarray,
1017
- transpose_segmentation: bool = False
1018
- ) -> tuple[int, Generator[BinaryIO, None, None]]:
1009
+ def _generate_segmentation_frames(file_path: str | np.ndarray,
1010
+ transpose_segmentation: bool = False
1011
+ ) -> tuple[int, Generator[np.ndarray, None, None]]:
1019
1012
  if not isinstance(file_path, (str, np.ndarray)):
1020
1013
  raise ValueError(f"Unsupported file type: {type(file_path)}")
1021
1014
 
@@ -1029,7 +1022,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1029
1022
  normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
1030
1023
 
1031
1024
  nframes = normalized_imgs.shape[3]
1032
- fios = AnnotationsApi._numpy_to_bytesio_png(normalized_imgs)
1025
+ frames = AnnotationsApi._numpy_to_rgb_frames(normalized_imgs)
1033
1026
 
1034
1027
  elif file_path.endswith(('.nii', '.nii.gz')):
1035
1028
  loaded_image: Any = nib_load(file_path)
@@ -1045,7 +1038,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1045
1038
  normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
1046
1039
 
1047
1040
  nframes = normalized_imgs.shape[3]
1048
- fios = AnnotationsApi._numpy_to_bytesio_png(normalized_imgs)
1041
+ frames = AnnotationsApi._numpy_to_rgb_frames(normalized_imgs)
1049
1042
 
1050
1043
  elif file_path.endswith('.png'):
1051
1044
  with Image.open(file_path) as img:
@@ -1055,12 +1048,12 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1055
1048
  if transpose_segmentation:
1056
1049
  normalized_imgs = normalized_imgs.transpose(0, 2, 1, 3)
1057
1050
 
1058
- fios = AnnotationsApi._numpy_to_bytesio_png(normalized_imgs)
1051
+ frames = AnnotationsApi._numpy_to_rgb_frames(normalized_imgs)
1059
1052
  nframes = 1
1060
1053
  else:
1061
1054
  raise ValueError(f"Unsupported file format of '{file_path}'")
1062
1055
 
1063
- return nframes, fios
1056
+ return nframes, frames
1064
1057
 
1065
1058
  @staticmethod
1066
1059
  def _normalize_segmentation_array(seg_imgs: np.ndarray) -> np.ndarray:
@@ -1087,15 +1080,15 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1087
1080
  return seg_imgs
1088
1081
 
1089
1082
  @staticmethod
1090
- def _numpy_to_bytesio_png(seg_imgs: np.ndarray) -> Generator[BinaryIO, None, None]:
1083
+ def _numpy_to_rgb_frames(seg_imgs: np.ndarray) -> Generator[np.ndarray, None, None]:
1091
1084
  """
1092
- Convert normalized segmentation images to PNG BytesIO objects.
1085
+ Convert normalized segmentation images to RGB frames.
1093
1086
 
1094
1087
  Args:
1095
1088
  seg_imgs: Normalized segmentation array in shape (channels, height, width, frames).
1096
1089
 
1097
1090
  Yields:
1098
- BinaryIO: PNG image data as BytesIO objects
1091
+ np.ndarray: uint8 array of shape (height, width, 3)
1099
1092
  """
1100
1093
  # PIL RGB format is: (height, width, channels)
1101
1094
  if seg_imgs.shape[0] not in [1, 3, 4]:
@@ -1104,13 +1097,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
1104
1097
  for i in range(nframes):
1105
1098
  img = seg_imgs[:, :, :, i].astype(np.uint8)
1106
1099
  if img.shape[0] == 1:
1107
- pil_img = Image.fromarray(img[0]).convert('RGB')
1108
- else:
1109
- pil_img = Image.fromarray(img.transpose(1, 2, 0))
1110
- img_bytes = BytesIO()
1111
- pil_img.save(img_bytes, format='PNG')
1112
- img_bytes.seek(0)
1113
- yield img_bytes
1100
+ img = np.repeat(img, 3, axis=0)
1101
+ yield np.ascontiguousarray(img[:3].transpose(1, 2, 0))
1114
1102
 
1115
1103
  def create_image_classification(self,
1116
1104
  resource: str | Resource,
@@ -1,10 +1,12 @@
1
1
  import logging
2
2
  from collections.abc import Sequence
3
+ from datetime import date
3
4
  from typing import TYPE_CHECKING, Any, Literal, overload
4
5
 
5
6
  from typing_extensions import override
6
7
 
7
8
  from datamint.entities.annotation_worklist import AnnotationWorklist
9
+ from datamint.utils.date_utils import to_iso_date_str
8
10
 
9
11
  from ..entity_base_api import CreatableEntityApi, UpdatableEntityApi
10
12
 
@@ -723,8 +725,8 @@ class AnnotationWorklistApi(CreatableEntityApi[AnnotationWorklist],
723
725
  def download_annotations(
724
726
  self,
725
727
  worklist_id: str | None = None,
726
- from_date: str | None = None,
727
- to_date: str | None = None,
728
+ from_date: date | str | None = None,
729
+ to_date: date | str | None = None,
728
730
  annotators: list[str] | None = None,
729
731
  annotations: list[str] | None = None,
730
732
  format: str = 'csv',
@@ -733,8 +735,8 @@ class AnnotationWorklistApi(CreatableEntityApi[AnnotationWorklist],
733
735
 
734
736
  Args:
735
737
  worklist_id: The annotation worklist ID.
736
- from_date: Optional start date filter (ISO string).
737
- to_date: Optional end date filter (ISO string).
738
+ from_date: Optional start date filter (``date`` or ISO date string).
739
+ to_date: Optional end date filter (``date`` or ISO date string).
738
740
  annotators: Optional list of annotator emails. Accepts either a repeated/array
739
741
  input or a comma-separated string.
740
742
  annotations: Optional list of annotation identifiers. Accepts either a repeated/array
@@ -749,9 +751,9 @@ class AnnotationWorklistApi(CreatableEntityApi[AnnotationWorklist],
749
751
 
750
752
  params: dict[str, str | list[str]] = {'format': format}
751
753
  if from_date is not None:
752
- params['from'] = from_date
754
+ params['from'] = to_iso_date_str(from_date)
753
755
  if to_date is not None:
754
- params['to'] = to_date
756
+ params['to'] = to_iso_date_str(to_date)
755
757
  if annotators is not None:
756
758
  params['annotators[]'] = annotators
757
759
  if annotations is not None:
@@ -1,5 +1,6 @@
1
1
  import uuid
2
2
  from collections.abc import Sequence
3
+ from datetime import date
3
4
  from pathlib import Path
4
5
  from typing import TYPE_CHECKING, Literal, overload
5
6
 
@@ -8,6 +9,7 @@ import httpx
8
9
  from datamint.entities.project import Project
9
10
  from datamint.entities.project_resource_split import ProjectResourceSplit
10
11
  from datamint.entities.resource import Resource
12
+ from datamint.utils.date_utils import to_iso_date_str
11
13
  from datamint.exceptions import (
12
14
  DefaultProjectNotSetError,
13
15
  EntityAlreadyExistsError,
@@ -544,8 +546,8 @@ class ProjectsApi(CRUDEntityApi[Project]):
544
546
  format: str = 'csv',
545
547
  annotators: list[str] | None = None,
546
548
  annotations: list[str] | None = None,
547
- from_date: str | None = None,
548
- to_date: str | None = None,
549
+ from_date: date | str | None = None,
550
+ to_date: date | str | None = None,
549
551
  progress_bar: bool = True,
550
552
  project: str | Project | None = None) -> None:
551
553
  """Download annotation data as a CSV or Excel file.
@@ -555,8 +557,8 @@ class ProjectsApi(CRUDEntityApi[Project]):
555
557
  format: Export format, ``'csv'`` (default) or ``'xlsx'``.
556
558
  annotators: Optional list of annotator emails to include.
557
559
  annotations: Optional list of annotation identifiers to include.
558
- from_date: Optional start date filter (ISO string).
559
- to_date: Optional end date filter (ISO string).
560
+ from_date: Optional start date filter (``date`` or ISO date string).
561
+ to_date: Optional end date filter (``date`` or ISO date string).
560
562
  progress_bar: Whether to display a progress bar.
561
563
  project: The project ID or Project instance. Falls back to the
562
564
  session's default project (see `datamint.select_project()`) when omitted.
@@ -566,9 +568,9 @@ class ProjectsApi(CRUDEntityApi[Project]):
566
568
  proj_id = self._entid(self._resolve_project_or_default(project))
567
569
  params: dict = {'format': format}
568
570
  if from_date is not None:
569
- params['from'] = from_date
571
+ params['from'] = to_iso_date_str(from_date)
570
572
  if to_date is not None:
571
- params['to'] = to_date
573
+ params['to'] = to_iso_date_str(to_date)
572
574
  if annotators is not None:
573
575
  params['annotators[]'] = annotators
574
576
  if annotations is not None:
@@ -30,6 +30,7 @@ from datamint.entities.annotations import AnnotationType
30
30
  from datamint.entities.annotations.annotation import Annotation
31
31
  from datamint.exceptions import ItemNotFoundError, ServerError, ValidationError
32
32
  from datamint.types import ImagingData
33
+ from datamint.utils.date_utils import to_iso_date_str
33
34
  from datamint.utils.collection_utils import ChainedSequence
34
35
 
35
36
  from ..base_api import ApiConfig, BaseApi
@@ -191,22 +192,10 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
191
192
  'include' returns both. Deleted resources are only visible to admins and librarians.
192
193
  """
193
194
 
194
- # Convert datetime objects to ISO format
195
- if from_date:
196
- if isinstance(from_date, str):
197
- date.fromisoformat(from_date)
198
- else:
199
- from_date = from_date.isoformat()
200
- if to_date:
201
- if isinstance(to_date, str):
202
- date.fromisoformat(to_date)
203
- else:
204
- to_date = to_date.isoformat()
205
-
206
195
  # Prepare the payload
207
196
  payload = {
208
- "from": from_date,
209
- "to": to_date,
197
+ "from": to_iso_date_str(from_date),
198
+ "to": to_iso_date_str(to_date),
210
199
  "status": status if status is not None else "",
211
200
  "modality": modality,
212
201
  "mimetype": mimetype,
@@ -20,7 +20,7 @@ from pydantic import BeforeValidator, Field, PlainSerializer
20
20
 
21
21
  from datamint.types import CacheMode, ImagingData
22
22
 
23
- from .annotation import Annotation
23
+ from .annotation import Annotation, _normalize_annotation_data
24
24
 
25
25
  _LOGGER = logging.getLogger(__name__)
26
26
 
@@ -286,6 +286,63 @@ class BaseSegmentationAnnotation(Annotation):
286
286
  'Use upload_segmentations or upload_volume_segmentation instead.'
287
287
  )
288
288
 
289
+ @classmethod
290
+ def from_dict(cls, data: dict[str, Any]) -> 'BaseSegmentationAnnotation':
291
+ """Create a segmentation annotation from a dictionary.
292
+
293
+ This overrides :meth:`Annotation.from_dict`, which rejects
294
+ segmentation annotations that have no associated ``file``. A
295
+ segmentation payload may instead carry the mask inline through the
296
+ ``segmentation_data`` field (or its ``mask`` alias), as produced by
297
+ :meth:`BaseEntity.asdict` / ``model_dump``. Such payloads are
298
+ accepted here and the mask is decoded by the Pydantic
299
+ ``BeforeValidator`` on :attr:`segmentation_data`.
300
+
301
+ Args:
302
+ data: Dictionary containing annotation data, optionally including
303
+ an inline ``segmentation_data`` (or ``mask``) payload.
304
+
305
+ Returns:
306
+ A segmentation annotation instance with ``segmentation_data``
307
+ decoded to a native object (``np.ndarray``, ``PIL.Image.Image``
308
+ or ``Nifti1Image``).
309
+
310
+ Raises:
311
+ ValueError: If neither an associated ``file`` nor an inline
312
+ ``segmentation_data`` payload is present.
313
+
314
+ Example:
315
+ >>> import json
316
+ >>> from datamint.entities.annotations import ImageSegmentation
317
+ >>> with open('annotation.json') as f:
318
+ ... raw = json.load(f) # list[list[dict]]
319
+ >>> seg = ImageSegmentation.from_dict(raw[0][0])
320
+ >>> seg.segmentation_data.shape
321
+ """
322
+ converted_data = _normalize_annotation_data(data)
323
+
324
+ has_inline_mask = (
325
+ converted_data.get('segmentation_data') is not None
326
+ or converted_data.get('mask') is not None
327
+ )
328
+
329
+ if converted_data.get('file') is None and not has_inline_mask:
330
+ raise ValueError(
331
+ "Segmentation annotations must have an associated file or an "
332
+ f"inline segmentation_data payload. {data}"
333
+ )
334
+
335
+ # Accept both field names and their aliases (e.g. ``mask`` for
336
+ # ``segmentation_data``) so inline payloads are not silently dropped.
337
+ valid_fields = set(cls.model_fields)
338
+ for field in cls.model_fields.values():
339
+ if field.alias:
340
+ valid_fields.add(field.alias)
341
+
342
+ filtered_data = {k: v for k, v in converted_data.items() if k in valid_fields}
343
+
344
+ return cls(**filtered_data)
345
+
289
346
  @overload
290
347
  def fetch_file_data(
291
348
  self,
@@ -281,28 +281,35 @@ class BaseDatamintModel(PythonModel, ABC):
281
281
  :class:`~datamint.mlflow.flavors.prediction_router.DeployedModelPrompts` instance before dispatch,
282
282
  so ``predict_*`` handlers receive a typed object rather than a raw dict.
283
283
  """
284
- params = dict(params or {})
285
- log_predictions = params.pop('log_predictions', False)
286
- model_name = params.pop('model_name', None)
287
-
288
- raw_prompts = params.pop('prompts', None)
289
- if raw_prompts is not None:
290
-
291
- # Sent JSON-encoded
292
- if isinstance(raw_prompts, str):
293
- raw_prompts = json.loads(raw_prompts)
294
- params['prompts'] = (
295
- raw_prompts
296
- if isinstance(raw_prompts, DeployedModelPrompts)
297
- else DeployedModelPrompts.from_dict(raw_prompts)
298
- )
284
+ logger.info(f'Predicting on {len(model_input)} resources')
285
+ try:
286
+ params = dict(params or {})
287
+ log_predictions = params.pop('log_predictions', False)
288
+ model_name = params.pop('model_name', None)
289
+
290
+ raw_prompts = params.pop('prompts', None)
291
+ if raw_prompts is not None:
292
+
293
+ # Sent JSON-encoded
294
+ if isinstance(raw_prompts, str):
295
+ raw_prompts = json.loads(raw_prompts)
296
+ params['prompts'] = (
297
+ raw_prompts
298
+ if isinstance(raw_prompts, DeployedModelPrompts)
299
+ else DeployedModelPrompts.from_dict(raw_prompts)
300
+ )
301
+
299
302
 
300
- result = self._router.dispatch(model_input, params)
303
+ result = self._router.dispatch(model_input, params)
301
304
 
302
- if log_predictions:
303
- self._log_predictions(model_input, result, model_name=model_name)
305
+ if log_predictions:
306
+ self._log_predictions(model_input, result, model_name=model_name)
304
307
 
305
- return result
308
+ return result
309
+ except Exception as e:
310
+ logger.error(f'Failed to predict on {len(model_input)} resources: {e}')
311
+ logger.exception(e)
312
+ raise
306
313
 
307
314
  def _log_predictions(
308
315
  self,
@@ -0,0 +1,21 @@
1
+ from datetime import date
2
+
3
+
4
+ def to_iso_date_str(value: date | str | None) -> str | None:
5
+ """Normalize a date filter to an ISO 8601 string.
6
+
7
+ Args:
8
+ value: A ``date``/``datetime`` or an ISO date string (``YYYY-MM-DD``).
9
+
10
+ Returns:
11
+ The ISO string, or ``None`` if ``value`` is ``None``.
12
+
13
+ Raises:
14
+ ValueError: If ``value`` is a string that is not a valid ISO date.
15
+ """
16
+ if value is None:
17
+ return None
18
+ if isinstance(value, str):
19
+ date.fromisoformat(value)
20
+ return value
21
+ return value.isoformat()