Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions datamint/api/endpoints/annotations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
CoordinateSystem,
ImageClassification,
LineAnnotation,
NumericAnnotation,
annotation_from_dict,
)
from datamint.exceptions import ItemNotFoundError, ServerError
Expand Down Expand Up @@ -1151,6 +1152,46 @@ def create_image_classification(self,
raise TypeError('Expected a single annotation id for image classification creation.')
return created

def create_numeric_annotation(self,
resource: str | Resource,
identifier: str,
value: int | float,
units: str | None = None,
imported_from: str | None = None,
model_id: str | None = None,
source: str | None = 'imported',
) -> str:
"""
Create a numeric value annotation (e.g. a measurement or count).

Args:
resource: The resource unique id or Resource instance.
identifier: The annotation identifier/label.
value: The numeric value. ``int`` maps to :attr:`AnnotationType.INTEGER`,
``float`` maps to :attr:`AnnotationType.FLOAT`.
units: Optional unit label for the value (e.g. 'years', 'mm').
imported_from: The imported from source value.
model_id: The model unique id.
source: Annotation source tag. Defaults to 'imported' since this is a direct API
entry point; :meth:`upload_predictions` overrides it with 'model_pipeline'/'model_deploy'.

Returns:
The id of the created annotation.
"""
annotation = NumericAnnotation(
name=identifier,
value=value,
units=units,
imported_from=imported_from,
model_id=model_id,
source=source,
)

created = self.create(resource, annotation)
if not isinstance(created, str):
raise TypeError('Expected a single annotation id for numeric annotation creation.')
return created

def upload_predictions(
self,
resource: str | Resource,
Expand Down
9 changes: 9 additions & 0 deletions datamint/entities/annotations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .box_annotation import BoxAnnotation
from .geometry import BoxGeometry, CoordinateSystem, Geometry, LineGeometry
from .line_annotation import LineAnnotation
from .numeric_annotation import NumericAnnotation
from .volume_segmentation import VolumeSegmentation
from .types import AnnotationType

Expand All @@ -15,6 +16,10 @@ def annotation_from_dict(data: dict) -> Annotation:

* ``'segmentation'`` with a ``class_map`` → :class:`VolumeSegmentation`
* ``'segmentation'`` without ``class_map`` → :class:`ImageSegmentation`
* ``'category'`` → :class:`ImageClassification`
* ``'integer'``/``'float'`` → :class:`NumericAnnotation`
* ``'line'`` → :class:`LineAnnotation`
* ``'square'`` → :class:`BoxAnnotation`
* anything else → :class:`Annotation`

``segmentation_data`` dicts are automatically deserialised by the
Expand All @@ -40,6 +45,9 @@ def annotation_from_dict(data: dict) -> Annotation:
if annotation_type in (AnnotationType.CATEGORY, AnnotationType.CATEGORY.value):
return ImageClassification(**normalized_data)

if annotation_type in (AnnotationType.INTEGER, AnnotationType.FLOAT):
return NumericAnnotation(**normalized_data)

if annotation_type in (AnnotationType.LINE, AnnotationType.LINE.value):
return LineAnnotation(**normalized_data)

Expand All @@ -59,6 +67,7 @@ def annotation_from_dict(data: dict) -> Annotation:
"Geometry",
"LineAnnotation",
"LineGeometry",
"NumericAnnotation",
"VolumeSegmentation",
"AnnotationType",
"annotation_from_dict",
Expand Down
7 changes: 6 additions & 1 deletion datamint/entities/annotations/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def _normalize_annotation_data(data: dict[str, Any]) -> dict[str, Any]:
if 'scope' not in converted_data:
converted_data['scope'] = 'image' if converted_data.get('frame_index') is None else 'frame'

if converted_data.get('annotation_type') in (AnnotationType.INTEGER, AnnotationType.FLOAT):
raw_value = converted_data.pop('text_value', None)
if raw_value is not None and converted_data.get('numeric_value') is None:
converted_data['numeric_value'] = raw_value

return converted_data


Expand Down Expand Up @@ -290,7 +295,7 @@ def _to_create_dto(self):
identifier=self.identifier,
scope=self.scope,
annotation_worklist_id=self.annotation_worklist_id,
value=self.text_value,
value=self.text_value if self.text_value is not None else self.numeric_value,
imported_from=self.imported_from,
import_author=self.import_author,
frame_index=self.frame_index,
Expand Down
30 changes: 30 additions & 0 deletions datamint/entities/annotations/numeric_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

from typing import Any

from .annotation import Annotation
from .types import AnnotationType


class NumericAnnotation(Annotation):
def __init__(
self,
name: str | None = None,
value: int | float | None = None,
units: str | None = None,
confiability: float = 1.0,
**kwargs: Any,
) -> None:
if name is not None:
kwargs.setdefault('identifier', name)
if value is not None:
kwargs.setdefault('numeric_value', value)
is_int = isinstance(value, int) and not isinstance(value, bool)
kwargs.setdefault('annotation_type', AnnotationType.INTEGER if is_int else AnnotationType.FLOAT)

if units is not None:
kwargs.setdefault('units', units)

kwargs.setdefault('confiability', confiability)
kwargs.setdefault('scope', 'image')
super().__init__(**kwargs)
4 changes: 3 additions & 1 deletion datamint/entities/annotations/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ class AnnotationType(StrEnum):
SQUARE = 'square'
CIRCLE = 'circle'
CATEGORY = 'category'
LABEL = 'label'
LABEL = 'label'
INTEGER = 'integer'
FLOAT = 'float'
Loading