datamint 2.3.3__py3-none-any.whl → 2.9.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.
- datamint/__init__.py +1 -3
- datamint/api/__init__.py +0 -3
- datamint/api/base_api.py +286 -54
- datamint/api/client.py +76 -13
- datamint/api/endpoints/__init__.py +2 -2
- datamint/api/endpoints/annotations_api.py +186 -28
- datamint/api/endpoints/deploy_model_api.py +78 -0
- datamint/api/endpoints/models_api.py +1 -0
- datamint/api/endpoints/projects_api.py +38 -7
- datamint/api/endpoints/resources_api.py +227 -100
- datamint/api/entity_base_api.py +66 -7
- datamint/apihandler/base_api_handler.py +0 -1
- datamint/apihandler/dto/annotation_dto.py +2 -0
- datamint/client_cmd_tools/datamint_config.py +0 -1
- datamint/client_cmd_tools/datamint_upload.py +3 -1
- datamint/configs.py +11 -7
- datamint/dataset/base_dataset.py +24 -4
- datamint/dataset/dataset.py +1 -1
- datamint/entities/__init__.py +1 -1
- datamint/entities/annotations/__init__.py +13 -0
- datamint/entities/{annotation.py → annotations/annotation.py} +81 -47
- datamint/entities/annotations/image_classification.py +12 -0
- datamint/entities/annotations/image_segmentation.py +252 -0
- datamint/entities/annotations/volume_segmentation.py +273 -0
- datamint/entities/base_entity.py +100 -6
- datamint/entities/cache_manager.py +129 -15
- datamint/entities/datasetinfo.py +60 -65
- datamint/entities/deployjob.py +18 -0
- datamint/entities/project.py +39 -0
- datamint/entities/resource.py +310 -46
- datamint/lightning/__init__.py +1 -0
- datamint/lightning/datamintdatamodule.py +103 -0
- datamint/mlflow/__init__.py +65 -0
- datamint/mlflow/artifact/__init__.py +1 -0
- datamint/mlflow/artifact/datamint_artifacts_repo.py +8 -0
- datamint/mlflow/env_utils.py +131 -0
- datamint/mlflow/env_vars.py +5 -0
- datamint/mlflow/flavors/__init__.py +17 -0
- datamint/mlflow/flavors/datamint_flavor.py +150 -0
- datamint/mlflow/flavors/model.py +877 -0
- datamint/mlflow/lightning/callbacks/__init__.py +1 -0
- datamint/mlflow/lightning/callbacks/modelcheckpoint.py +410 -0
- datamint/mlflow/models/__init__.py +93 -0
- datamint/mlflow/tracking/datamint_store.py +76 -0
- datamint/mlflow/tracking/default_experiment.py +27 -0
- datamint/mlflow/tracking/fluent.py +91 -0
- datamint/utils/env.py +27 -0
- datamint/utils/visualization.py +21 -13
- datamint-2.9.0.dist-info/METADATA +220 -0
- datamint-2.9.0.dist-info/RECORD +73 -0
- {datamint-2.3.3.dist-info → datamint-2.9.0.dist-info}/WHEEL +1 -1
- datamint-2.9.0.dist-info/entry_points.txt +18 -0
- datamint/apihandler/exp_api_handler.py +0 -204
- datamint/experiment/__init__.py +0 -1
- datamint/experiment/_patcher.py +0 -570
- datamint/experiment/experiment.py +0 -1049
- datamint-2.3.3.dist-info/METADATA +0 -125
- datamint-2.3.3.dist-info/RECORD +0 -54
- datamint-2.3.3.dist-info/entry_points.txt +0 -4
datamint/api/entity_base_api.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from typing import Any, TypeVar, Generic,
|
|
1
|
+
from typing import Any, Literal, TypeVar, Generic, overload
|
|
2
|
+
from collections.abc import Sequence, AsyncGenerator
|
|
2
3
|
import logging
|
|
3
4
|
import httpx
|
|
4
5
|
from datamint.entities.base_entity import BaseEntity
|
|
@@ -7,7 +8,6 @@ import aiohttp
|
|
|
7
8
|
import asyncio
|
|
8
9
|
from .base_api import ApiConfig, BaseApi
|
|
9
10
|
import contextlib
|
|
10
|
-
from typing import AsyncGenerator
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
T = TypeVar('T', bound=BaseEntity)
|
|
@@ -24,9 +24,10 @@ class EntityBaseApi(BaseApi, Generic[T]):
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
def __init__(self, config: ApiConfig,
|
|
27
|
-
entity_class:
|
|
27
|
+
entity_class: type[T],
|
|
28
28
|
endpoint_base: str,
|
|
29
|
-
client: httpx.Client | None = None
|
|
29
|
+
client: httpx.Client | None = None
|
|
30
|
+
) -> None:
|
|
30
31
|
"""Initialize the entity API handler.
|
|
31
32
|
|
|
32
33
|
Args:
|
|
@@ -53,6 +54,11 @@ class EntityBaseApi(BaseApi, Generic[T]):
|
|
|
53
54
|
entity_id: str | BaseEntity,
|
|
54
55
|
add_path: str = '',
|
|
55
56
|
**kwargs) -> httpx.Response:
|
|
57
|
+
"""
|
|
58
|
+
Make an HTTP request for a specific entity by its ID.
|
|
59
|
+
It is basically a wrapper around :py:meth:`_make_request` that
|
|
60
|
+
constructs the URL for the entity in this form: `/{endpoint_base}/{entity_id}/{add_path}`
|
|
61
|
+
"""
|
|
56
62
|
try:
|
|
57
63
|
entity_id = self._entid(entity_id)
|
|
58
64
|
add_path = '/'.join(add_path.strip().strip('/').split('/'))
|
|
@@ -61,6 +67,10 @@ class EntityBaseApi(BaseApi, Generic[T]):
|
|
|
61
67
|
if e.response.status_code == 404:
|
|
62
68
|
raise ResourceNotFoundError(self.endpoint_base, {'id': entity_id}) from e
|
|
63
69
|
raise
|
|
70
|
+
except ResourceNotFoundError as e:
|
|
71
|
+
e.resource_type = self.endpoint_base
|
|
72
|
+
e.params = {'id': entity_id}
|
|
73
|
+
raise
|
|
64
74
|
|
|
65
75
|
@contextlib.asynccontextmanager
|
|
66
76
|
async def _make_entity_request_async(self,
|
|
@@ -81,6 +91,23 @@ class EntityBaseApi(BaseApi, Generic[T]):
|
|
|
81
91
|
if e.status == 404:
|
|
82
92
|
raise ResourceNotFoundError(self.endpoint_base, {'id': entity_id}) from e
|
|
83
93
|
raise
|
|
94
|
+
except ResourceNotFoundError as e:
|
|
95
|
+
e.resource_type = self.endpoint_base
|
|
96
|
+
e.params = {'id': entity_id}
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
async def _make_entity_request_async_json(self,
|
|
100
|
+
method: str,
|
|
101
|
+
entity_id: str | BaseEntity,
|
|
102
|
+
add_path: str = '',
|
|
103
|
+
session: aiohttp.ClientSession | None = None,
|
|
104
|
+
**kwargs):
|
|
105
|
+
async with self._make_entity_request_async(method,
|
|
106
|
+
entity_id,
|
|
107
|
+
add_path=add_path,
|
|
108
|
+
session=session,
|
|
109
|
+
**kwargs) as resp:
|
|
110
|
+
return await resp.json()
|
|
84
111
|
|
|
85
112
|
def _stream_entity_request(self,
|
|
86
113
|
method: str,
|
|
@@ -210,7 +237,9 @@ class DeletableEntityApi(EntityBaseApi[T]):
|
|
|
210
237
|
httpx.HTTPStatusError: If deletion fails or any entity not found
|
|
211
238
|
"""
|
|
212
239
|
async def _delete_all_async():
|
|
213
|
-
|
|
240
|
+
connector = self._create_aiohttp_connector(force_close=True)
|
|
241
|
+
timeout = aiohttp.ClientTimeout(total=None, connect=60, sock_read=300)
|
|
242
|
+
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
|
214
243
|
tasks = [
|
|
215
244
|
self._delete_async(entity, session)
|
|
216
245
|
for entity in entities
|
|
@@ -248,7 +277,16 @@ class CreatableEntityApi(EntityBaseApi[T]):
|
|
|
248
277
|
This class adds methods to handle creation of new entities.
|
|
249
278
|
"""
|
|
250
279
|
|
|
251
|
-
|
|
280
|
+
@overload
|
|
281
|
+
def _create(self, entity_data: dict[str, Any],
|
|
282
|
+
return_entity: Literal[True] = True) -> T | list[T]: ...
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
def _create(self, entity_data: dict[str, Any],
|
|
286
|
+
return_entity: Literal[False]) -> str | list: ...
|
|
287
|
+
|
|
288
|
+
def _create(self, entity_data: dict[str, Any],
|
|
289
|
+
return_entity: bool = False) -> str | T | list:
|
|
252
290
|
"""Create a new entity.
|
|
253
291
|
|
|
254
292
|
Args:
|
|
@@ -263,14 +301,35 @@ class CreatableEntityApi(EntityBaseApi[T]):
|
|
|
263
301
|
response = self._make_request('POST', f'/{self.endpoint_base}', json=entity_data)
|
|
264
302
|
respdata = response.json()
|
|
265
303
|
if isinstance(respdata, str):
|
|
304
|
+
if return_entity:
|
|
305
|
+
return self.get_by_id(respdata)
|
|
266
306
|
return respdata
|
|
267
307
|
if isinstance(respdata, list):
|
|
308
|
+
if return_entity:
|
|
309
|
+
logger.warning("Current implementation is slow when returning entities on bulk create."
|
|
310
|
+
" Try ``return_entity=False`` for better performance.")
|
|
311
|
+
return [self.get_by_id(item['id']) if isinstance(item, dict) and 'id' in item else self.get_by_id(item)
|
|
312
|
+
for item in respdata]
|
|
268
313
|
return respdata
|
|
269
314
|
if isinstance(respdata, dict):
|
|
315
|
+
if return_entity:
|
|
316
|
+
try:
|
|
317
|
+
return self._init_entity_obj(**respdata)
|
|
318
|
+
except:
|
|
319
|
+
logger.debug("Failed to init entity obj on create response. Falling back to get_by_id.")
|
|
320
|
+
return self.get_by_id(respdata.get('id'))
|
|
270
321
|
return respdata.get('id')
|
|
271
322
|
return respdata
|
|
272
323
|
|
|
273
|
-
|
|
324
|
+
@overload
|
|
325
|
+
def create(self, *args, return_entity: Literal[True] = True, **kwargs) -> T: ...
|
|
326
|
+
|
|
327
|
+
@overload
|
|
328
|
+
def create(self, *args, return_entity: Literal[False], **kwargs) -> str: ...
|
|
329
|
+
|
|
330
|
+
def create(self, *args,
|
|
331
|
+
return_entity: bool = True,
|
|
332
|
+
**kwargs) -> str | T:
|
|
274
333
|
raise NotImplementedError("Subclasses must implement the create method with their own custom parameters")
|
|
275
334
|
|
|
276
335
|
|
|
@@ -178,6 +178,8 @@ class CreateAnnotationDto:
|
|
|
178
178
|
if model_id is not None:
|
|
179
179
|
if is_model == False:
|
|
180
180
|
raise ValueError("model_id==False while self.model_id is provided.")
|
|
181
|
+
if not isinstance(model_id, str):
|
|
182
|
+
raise ValueError("model_id must be a string if provided.")
|
|
181
183
|
is_model = True
|
|
182
184
|
self.is_model = is_model
|
|
183
185
|
self.geometry = geometry
|
|
@@ -574,6 +574,8 @@ def _parse_args() -> tuple[Any, list[str], Optional[list[dict]], Optional[list[s
|
|
|
574
574
|
help='Automatically detect and include JSON metadata files with the same base name as NIFTI files')
|
|
575
575
|
parser.add_argument('--no-auto-detect-json', dest='auto_detect_json', action='store_false',
|
|
576
576
|
help='Disable automatic detection of JSON metadata files (default behavior)')
|
|
577
|
+
parser.add_argument('--no-assemble-dicoms', dest='assemble_dicoms', action='store_false', default=True,
|
|
578
|
+
help='Do not assemble DICOM files into series (default: assemble them)')
|
|
577
579
|
parser.add_argument('--version', action='version', version=f'%(prog)s {datamint_version}')
|
|
578
580
|
parser.add_argument('--verbose', action='store_true', help='Print debug messages', default=False)
|
|
579
581
|
args = parser.parse_args()
|
|
@@ -797,7 +799,7 @@ def main():
|
|
|
797
799
|
publish_to=args.project,
|
|
798
800
|
segmentation_files=segfiles,
|
|
799
801
|
transpose_segmentation=args.transpose_segmentation,
|
|
800
|
-
assemble_dicoms=
|
|
802
|
+
assemble_dicoms=args.assemble_dicoms,
|
|
801
803
|
metadata=metadata_files,
|
|
802
804
|
progress_bar=True
|
|
803
805
|
)
|
datamint/configs.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import yaml
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
|
-
from netrc import netrc
|
|
5
4
|
from platformdirs import PlatformDirs
|
|
6
|
-
from typing import
|
|
7
|
-
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
8
6
|
|
|
9
7
|
APIURL_KEY = 'default_api_url'
|
|
10
8
|
APIKEY_KEY = 'api_key'
|
|
@@ -14,6 +12,10 @@ ENV_VARS = {
|
|
|
14
12
|
APIURL_KEY: 'DATAMINT_API_URL'
|
|
15
13
|
}
|
|
16
14
|
|
|
15
|
+
DEFAULT_VALUES = {
|
|
16
|
+
APIURL_KEY: 'https://api.datamint.io'
|
|
17
|
+
}
|
|
18
|
+
|
|
17
19
|
_LOGGER = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
DIRS = PlatformDirs(appname='datamintapi')
|
|
@@ -23,17 +25,19 @@ try:
|
|
|
23
25
|
except Exception as e:
|
|
24
26
|
_LOGGER.error(f"Could not determine home directory: {e}")
|
|
25
27
|
DATAMINT_DATA_DIR = None
|
|
26
|
-
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def get_env_var_name(key: str) -> str:
|
|
30
31
|
return ENV_VARS[key]
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
def read_config() -> dict[str, Any]:
|
|
33
35
|
if os.path.exists(CONFIG_FILE):
|
|
34
36
|
with open(CONFIG_FILE, 'r') as configfile:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
config = yaml.safe_load(configfile)
|
|
38
|
+
config.update({k: v for k, v in DEFAULT_VALUES.items() if k not in config})
|
|
39
|
+
return config
|
|
40
|
+
return DEFAULT_VALUES.copy()
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
def set_value(key: str,
|
datamint/dataset/base_dataset.py
CHANGED
|
@@ -307,6 +307,10 @@ class DatamintBaseDataset:
|
|
|
307
307
|
self.image_lsets, self.image_lcodes = self._get_labels_set(framed=False)
|
|
308
308
|
worklist_id = self.get_info()['worklist_id']
|
|
309
309
|
groups: dict[str, dict] = self.api.annotationsets.get_segmentation_group(worklist_id)['groups']
|
|
310
|
+
if not groups:
|
|
311
|
+
self.seglabel_list = []
|
|
312
|
+
self.seglabel2code = {}
|
|
313
|
+
return
|
|
310
314
|
# order by 'index' key
|
|
311
315
|
max_index = max([g['index'] for g in groups.values()])
|
|
312
316
|
self.seglabel_list : list[str] = ['UNKNOWN'] * max_index # 1-based
|
|
@@ -928,6 +932,7 @@ class DatamintBaseDataset:
|
|
|
928
932
|
# Collect all segmentation annotations that need to be downloaded
|
|
929
933
|
segmentations_to_download = []
|
|
930
934
|
segmentation_paths = []
|
|
935
|
+
segmentation_resource_map = {} # Maps annotation ID to resource ID for cleanup
|
|
931
936
|
|
|
932
937
|
# update annotations in resources
|
|
933
938
|
for resource in self.images_metainfo:
|
|
@@ -954,6 +959,7 @@ class DatamintBaseDataset:
|
|
|
954
959
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
955
960
|
segmentations_to_download.append(ann)
|
|
956
961
|
segmentation_paths.append(filepath)
|
|
962
|
+
segmentation_resource_map[ann.id] = resource_id
|
|
957
963
|
|
|
958
964
|
# Process annotation changes
|
|
959
965
|
for ann in annotations_to_remove:
|
|
@@ -971,14 +977,28 @@ class DatamintBaseDataset:
|
|
|
971
977
|
_LOGGER.error(f"Error deleting annotation file {filepath}: {e}")
|
|
972
978
|
|
|
973
979
|
# Update resource annotations list - convert to Annotation objects
|
|
974
|
-
# resource['annotations'] = [Annotation.from_dict(ann) for ann in new_resource_annotations]
|
|
975
980
|
resource['annotations'] = new_resource_annotations
|
|
976
981
|
|
|
977
982
|
# Batch download all segmentation files
|
|
978
983
|
if segmentations_to_download:
|
|
979
984
|
_LOGGER.info(f"Downloading {len(segmentations_to_download)} segmentation files...")
|
|
980
|
-
self.api.annotations.download_multiple_files(
|
|
981
|
-
|
|
985
|
+
download_results = self.api.annotations.download_multiple_files(
|
|
986
|
+
segmentations_to_download, segmentation_paths
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
# Process failed downloads
|
|
990
|
+
failed_annotations = [result['annotation_id'] for result in download_results if not result['success']]
|
|
991
|
+
if failed_annotations:
|
|
992
|
+
_LOGGER.warning(f"Failed to download {len(failed_annotations)} annotations, removing them from metadata")
|
|
993
|
+
|
|
994
|
+
# Remove failed annotations from each resource's annotation list
|
|
995
|
+
for resource in self.images_metainfo:
|
|
996
|
+
resource['annotations'] = [
|
|
997
|
+
ann for ann in resource['annotations']
|
|
998
|
+
if ann.id not in failed_annotations
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
_LOGGER.info(f"Successfully downloaded {len(segmentations_to_download) - len(failed_annotations)} segmentation files.")
|
|
982
1002
|
|
|
983
1003
|
###################
|
|
984
1004
|
# update metadata
|
|
@@ -1050,7 +1070,7 @@ class DatamintBaseDataset:
|
|
|
1050
1070
|
# delete associated annotations
|
|
1051
1071
|
for ann in deleted_metainfo.get('annotations', []):
|
|
1052
1072
|
ann_file = getattr(ann, 'file', None) if hasattr(ann, 'file') else ann.get('file', None)
|
|
1053
|
-
if ann_file is not None:
|
|
1073
|
+
if ann_file is not None and os.path.exists(os.path.join(self.dataset_dir, ann_file)):
|
|
1054
1074
|
os.remove(os.path.join(self.dataset_dir, ann_file))
|
|
1055
1075
|
|
|
1056
1076
|
def __add__(self, other):
|
datamint/dataset/dataset.py
CHANGED
|
@@ -7,7 +7,7 @@ import numpy as np
|
|
|
7
7
|
import logging
|
|
8
8
|
from PIL import Image
|
|
9
9
|
import albumentations
|
|
10
|
-
from datamint.entities.annotation import Annotation
|
|
10
|
+
from datamint.entities.annotations.annotation import Annotation
|
|
11
11
|
from medimgkit.readers import read_array_normalized
|
|
12
12
|
|
|
13
13
|
_LOGGER = logging.getLogger(__name__)
|
datamint/entities/__init__.py
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .image_classification import ImageClassification
|
|
2
|
+
from .image_segmentation import ImageSegmentation
|
|
3
|
+
from .annotation import Annotation
|
|
4
|
+
from .volume_segmentation import VolumeSegmentation
|
|
5
|
+
from datamint.api.dto import AnnotationType # FIXME: move this to this module
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ImageClassification",
|
|
9
|
+
"ImageSegmentation",
|
|
10
|
+
"Annotation",
|
|
11
|
+
"VolumeSegmentation",
|
|
12
|
+
"AnnotationType",
|
|
13
|
+
]
|
|
@@ -9,16 +9,17 @@ from typing import TYPE_CHECKING, Any
|
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
from
|
|
12
|
+
from ..base_entity import BaseEntity, MISSING_FIELD
|
|
13
|
+
from ..cache_manager import CacheManager
|
|
14
14
|
from pydantic import PrivateAttr
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
from datamint.api.dto import AnnotationType
|
|
17
17
|
from datamint.types import ImagingData
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from datamint.api.endpoints.annotations_api import AnnotationsApi
|
|
21
|
-
from
|
|
22
|
+
from ..resource import Resource
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
@@ -33,7 +34,28 @@ _FIELD_MAPPING = {
|
|
|
33
34
|
_ANNOTATION_CACHE_KEY = "annotation_data"
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
class
|
|
37
|
+
class AnnotationBase(BaseEntity):
|
|
38
|
+
"""Minimal base class for creating annotations.
|
|
39
|
+
|
|
40
|
+
This class contains only the essential fields needed to create annotations.
|
|
41
|
+
Use this for creating specific annotation types like ImageClassification.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
identifier: str
|
|
45
|
+
scope: str
|
|
46
|
+
annotation_type: AnnotationType
|
|
47
|
+
confiability: float = 1.0
|
|
48
|
+
|
|
49
|
+
def __init__(self, **data):
|
|
50
|
+
"""Initialize the annotation base entity."""
|
|
51
|
+
super().__init__(**data)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def name(self) -> str:
|
|
55
|
+
"""Get the annotation name (alias for identifier)."""
|
|
56
|
+
return self.identifier
|
|
57
|
+
|
|
58
|
+
class Annotation(AnnotationBase):
|
|
37
59
|
"""Pydantic Model representing a DataMint annotation.
|
|
38
60
|
|
|
39
61
|
Attributes:
|
|
@@ -67,32 +89,31 @@ class Annotation(BaseEntity):
|
|
|
67
89
|
values: Optional extra values payload for flexible schemas.
|
|
68
90
|
"""
|
|
69
91
|
|
|
70
|
-
id: str
|
|
92
|
+
id: str | None = None
|
|
71
93
|
identifier: str
|
|
72
94
|
scope: str
|
|
73
|
-
frame_index: int | None
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
user_info: dict | None
|
|
95
|
+
frame_index: int | None = None
|
|
96
|
+
text_value: str | None = None
|
|
97
|
+
numeric_value: float | int | None = None
|
|
98
|
+
units: str | None = None
|
|
99
|
+
geometry: list | dict | None = None
|
|
100
|
+
created_at: str | None = None # ISO timestamp string
|
|
101
|
+
created_by: str | None = None
|
|
102
|
+
annotation_worklist_id: str | None = None
|
|
103
|
+
status: str | None = None
|
|
104
|
+
approved_at: str | None = None # ISO timestamp string
|
|
105
|
+
approved_by: str | None = None
|
|
106
|
+
resource_id: str | None = None
|
|
107
|
+
associated_file: str | None = None
|
|
108
|
+
deleted: bool = False
|
|
109
|
+
deleted_at: str | None = None # ISO timestamp string
|
|
110
|
+
deleted_by: str | None = None
|
|
111
|
+
created_by_model: str | None = None
|
|
112
|
+
set_name: str | None = None
|
|
113
|
+
resource_filename: str | None = None
|
|
114
|
+
resource_modality: str | None = None
|
|
115
|
+
annotation_worklist_name: str | None = None
|
|
116
|
+
user_info: dict | None = None
|
|
96
117
|
values: list | None = MISSING_FIELD
|
|
97
118
|
file: str | None = None
|
|
98
119
|
|
|
@@ -101,9 +122,14 @@ class Annotation(BaseEntity):
|
|
|
101
122
|
def __init__(self, **data):
|
|
102
123
|
"""Initialize the annotation entity."""
|
|
103
124
|
super().__init__(**data)
|
|
104
|
-
self._cache: CacheManager = CacheManager('annotations')
|
|
105
125
|
self._resource: 'Resource | None' = None
|
|
106
126
|
|
|
127
|
+
@property
|
|
128
|
+
def _cache(self) -> CacheManager[bytes]:
|
|
129
|
+
if not hasattr(self, '__cache'):
|
|
130
|
+
self.__cache = CacheManager[bytes]('annotations')
|
|
131
|
+
return self.__cache
|
|
132
|
+
|
|
107
133
|
@property
|
|
108
134
|
def resource(self) -> 'Resource':
|
|
109
135
|
"""Lazily load and cache the associated Resource entity."""
|
|
@@ -117,24 +143,37 @@ class Annotation(BaseEntity):
|
|
|
117
143
|
auto_convert: bool = True,
|
|
118
144
|
use_cache: bool = False,
|
|
119
145
|
) -> bytes | ImagingData:
|
|
146
|
+
"""Get the file data for this annotation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
save_path: Optional path to save the file locally. If use_cache is also True,
|
|
150
|
+
the file is saved to save_path and cache metadata points to that location
|
|
151
|
+
(no duplication - only one file on disk).
|
|
152
|
+
auto_convert: If True, automatically converts to appropriate format
|
|
153
|
+
use_cache: If True, uses cached data when available and valid
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
File data (format depends on auto_convert and file type)
|
|
157
|
+
"""
|
|
120
158
|
# Version info for cache validation
|
|
121
159
|
version_info = self._generate_version_info()
|
|
122
160
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
img_data = self._cache.get(self.id, _ANNOTATION_CACHE_KEY, version_info)
|
|
127
|
-
|
|
128
|
-
if img_data is None:
|
|
129
|
-
# Fetch from server using download_resource_file
|
|
130
|
-
logger.debug(f"Fetching image data from server for resource {self.id}")
|
|
131
|
-
img_data = self._api.download_file(
|
|
161
|
+
# Download callback for the shared caching logic
|
|
162
|
+
def download_callback(path: str | None) -> bytes:
|
|
163
|
+
return self._api.download_file(
|
|
132
164
|
self,
|
|
133
|
-
fpath_out=
|
|
165
|
+
fpath_out=path
|
|
134
166
|
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
167
|
+
|
|
168
|
+
# Use shared caching logic from BaseEntity
|
|
169
|
+
img_data = self._fetch_and_cache_file_data(
|
|
170
|
+
cache_manager=self._cache,
|
|
171
|
+
data_key=_ANNOTATION_CACHE_KEY,
|
|
172
|
+
version_info=version_info,
|
|
173
|
+
download_callback=download_callback,
|
|
174
|
+
save_path=save_path,
|
|
175
|
+
use_cache=use_cache,
|
|
176
|
+
)
|
|
138
177
|
|
|
139
178
|
if auto_convert:
|
|
140
179
|
return self._api.convert_format(img_data)
|
|
@@ -190,11 +229,6 @@ class Annotation(BaseEntity):
|
|
|
190
229
|
"""Alias for :attr:`annotation_type`."""
|
|
191
230
|
return self.annotation_type
|
|
192
231
|
|
|
193
|
-
@property
|
|
194
|
-
def name(self) -> str:
|
|
195
|
-
"""Get the annotation name (alias for identifier)."""
|
|
196
|
-
return self.identifier
|
|
197
|
-
|
|
198
232
|
@property
|
|
199
233
|
def index(self) -> int | None:
|
|
200
234
|
"""Get the frame index (alias for frame_index)."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .annotation import Annotation
|
|
2
|
+
from datamint.api.dto import AnnotationType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ImageClassification(Annotation):
|
|
6
|
+
def __init__(self,
|
|
7
|
+
name: str,
|
|
8
|
+
value: str,
|
|
9
|
+
confiability: float = 1.0):
|
|
10
|
+
super().__init__(identifier=name, text_value=value, scope='image',
|
|
11
|
+
confiability=confiability,
|
|
12
|
+
annotation_type=AnnotationType.CATEGORY)
|