Skip to content

Commit 422dd6d

Browse files
authored
Merge pull request #999 from Labelbox/imuhammad/AL-5108-dicom-polyline
[AL-5108] Add DICOM polyline annotation kind
2 parents cb3daa9 + 93b169d commit 422dd6d

File tree

7 files changed

+245
-30
lines changed

7 files changed

+245
-30
lines changed

labelbox/data/annotation_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from .annotation import VideoClassificationAnnotation
1010
from .annotation import ObjectAnnotation
1111
from .annotation import VideoObjectAnnotation
12+
from .annotation import DICOMObjectAnnotation
13+
from .annotation import GroupKey
1214

1315
from .ner import ConversationEntity
1416
from .ner import DocumentEntity

labelbox/data/annotation_types/annotation.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from enum import Enum
23
from typing import Any, Dict, List, Optional, Union
34

45
from labelbox.data.mixins import ConfidenceNotSupportedMixin, ConfidenceMixin
@@ -66,7 +67,7 @@ class VideoObjectAnnotation(ObjectAnnotation, ConfidenceNotSupportedMixin):
6667
>>> end=Point(x=1, y=1)
6768
>>> ),
6869
>>> feature_schema_id="my-feature-schema-id"
69-
>>>)
70+
>>> )
7071
7172
Args:
7273
name (Optional[str])
@@ -97,3 +98,41 @@ class VideoClassificationAnnotation(ClassificationAnnotation):
9798
"""
9899
frame: int
99100
segment_index: Optional[int] = None
101+
102+
103+
class GroupKey(Enum):
104+
"""Group key for DICOM annotations
105+
"""
106+
AXIAL = "axial"
107+
SAGITTAL = "sagittal"
108+
CORONAL = "coronal"
109+
110+
111+
class DICOMObjectAnnotation(VideoObjectAnnotation):
112+
"""DICOM object annotation
113+
114+
>>> DICOMObjectAnnotation(
115+
>>> name="dicom_polyline",
116+
>>> frame=2,
117+
>>> value=lb_types.Line(points = [
118+
>>> lb_types.Point(x=680, y=100),
119+
>>> lb_types.Point(x=100, y=190),
120+
>>> lb_types.Point(x=190, y=220)
121+
>>> ]),
122+
>>> segment_index=0,
123+
>>> keyframe=True,
124+
>>> group_key=GroupKey.AXIAL
125+
>>> )
126+
127+
Args:
128+
name (Optional[str])
129+
feature_schema_id (Optional[Cuid])
130+
value (Geometry)
131+
group_key (GroupKey)
132+
frame (Int): The frame index that this annotation corresponds to
133+
keyframe (bool): Whether or not this annotation was a human generated or interpolated annotation
134+
segment_id (Optional[Int]): Index of video segment this annotation belongs to
135+
classifications (List[ClassificationAnnotation]) = []
136+
extra (Dict[str, Any])
137+
"""
138+
group_key: GroupKey

labelbox/data/annotation_types/label.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
from labelbox.data.annotation_types.data.tiled_image import TiledImageData
99
from labelbox.schema import ontology
1010
from .annotation import (ClassificationAnnotation, ObjectAnnotation,
11-
VideoClassificationAnnotation, VideoObjectAnnotation)
11+
VideoClassificationAnnotation, VideoObjectAnnotation,
12+
DICOMObjectAnnotation)
1213
from .classification import ClassificationAnswer
13-
from .data import VideoData, TextData, ImageData
14+
from .data import DicomData, VideoData, TextData, ImageData
1415
from .geometry import Mask
1516
from .metrics import ScalarMetric, ConfusionMatrixMetric
1617
from .types import Cuid
@@ -39,9 +40,7 @@ class Label(BaseModel):
3940
uid: Optional[Cuid] = None
4041
data: Union[VideoData, ImageData, TextData, TiledImageData]
4142
annotations: List[Union[ClassificationAnnotation, ObjectAnnotation,
42-
VideoObjectAnnotation,
43-
VideoClassificationAnnotation, ScalarMetric,
44-
ConfusionMatrixMetric]] = []
43+
ScalarMetric, ConfusionMatrixMetric]] = []
4544
extra: Dict[str, Any] = {}
4645

4746
def object_annotations(self) -> List[ObjectAnnotation]:

labelbox/data/serialization/ndjson/label.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66

77
from pydantic import BaseModel
88

9-
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation, VideoObjectAnnotation
9+
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation, VideoObjectAnnotation, DICOMObjectAnnotation
1010
from ...annotation_types.collection import LabelCollection, LabelGenerator
11-
from ...annotation_types.data import ImageData, TextData, VideoData
11+
from ...annotation_types.data import DicomData, ImageData, TextData, VideoData
1212
from ...annotation_types.label import Label
1313
from ...annotation_types.ner import TextEntity, ConversationEntity
1414
from ...annotation_types.classification import Dropdown
1515
from ...annotation_types.metrics import ScalarMetric, ConfusionMatrixMetric
1616

1717
from .metric import NDScalarMetric, NDMetricAnnotation, NDConfusionMatrixMetric
1818
from .classification import NDChecklistSubclass, NDClassification, NDClassificationType, NDRadioSubclass
19-
from .objects import NDObject, NDObjectType, NDSegments
19+
from .objects import NDObject, NDObjectType, NDSegments, NDDicomSegments
2020
from .base import DataRow
2121

2222

2323
class NDLabel(BaseModel):
2424
annotations: List[Union[NDObjectType, NDClassificationType,
2525
NDConfusionMatrixMetric, NDScalarMetric,
26-
NDSegments]]
26+
NDDicomSegments, NDSegments]]
2727

2828
def to_common(self) -> LabelGenerator:
2929
grouped_annotations = defaultdict(list)
@@ -52,7 +52,11 @@ def _generate_annotations(
5252
annots = []
5353
data_row = annotations[0].data_row
5454
for annotation in annotations:
55-
if isinstance(annotation, NDSegments):
55+
if isinstance(annotation, NDDicomSegments):
56+
annots.extend(
57+
NDDicomSegments.to_common(annotation, annotation.name,
58+
annotation.schema_id))
59+
elif isinstance(annotation, NDSegments):
5660
annots.extend(
5761
NDSegments.to_common(annotation, annotation.name,
5862
annotation.schema_id))
@@ -73,9 +77,9 @@ def _infer_media_type(
7377
self, data_row: DataRow,
7478
annotations: List[Union[TextEntity, ConversationEntity,
7579
VideoClassificationAnnotation,
76-
VideoObjectAnnotation, ObjectAnnotation,
77-
ClassificationAnnotation, ScalarMetric,
78-
ConfusionMatrixMetric]]
80+
DICOMObjectAnnotation, VideoObjectAnnotation,
81+
ObjectAnnotation, ClassificationAnnotation,
82+
ScalarMetric, ConfusionMatrixMetric]]
7983
) -> Union[TextData, VideoData, ImageData]:
8084
if len(annotations) == 0:
8185
raise ValueError("Missing annotations while inferring media type")
@@ -86,6 +90,8 @@ def _infer_media_type(
8690
data = TextData
8791
elif VideoClassificationAnnotation in types or VideoObjectAnnotation in types:
8892
data = VideoData
93+
elif DICOMObjectAnnotation in types:
94+
data = DicomData
8995

9096
if data_row.id:
9197
return data(uid=data_row.id)

labelbox/data/serialization/ndjson/objects.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ...annotation_types.ner import DocumentEntity, DocumentTextSelection, TextEntity
1717
from ...annotation_types.types import Cuid
1818
from ...annotation_types.geometry import Rectangle, Polygon, Line, Point, Mask
19-
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoObjectAnnotation
19+
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoObjectAnnotation, DICOMObjectAnnotation
2020
from .classification import NDSubclassification, NDSubclassificationType
2121
from .base import DataRow, NDAnnotation
2222

@@ -30,6 +30,10 @@ class VideoSupported(BaseModel):
3030
frame: int
3131

3232

33+
class DicomSupported(BaseModel):
34+
group_key: str
35+
36+
3337
class _Point(BaseModel):
3438
x: float
3539
y: float
@@ -136,6 +140,20 @@ def from_common(cls, frame: int, line: Line):
136140
} for pt in line.points])
137141

138142

143+
class NDDicomLine(NDFrameLine):
144+
145+
def to_common(self, name: str, feature_schema_id: Cuid, segment_index: int,
146+
group_key: str) -> DICOMObjectAnnotation:
147+
return DICOMObjectAnnotation(
148+
frame=self.frame,
149+
segment_index=segment_index,
150+
keyframe=True,
151+
name=name,
152+
feature_schema_id=feature_schema_id,
153+
value=Line(points=[Point(x=pt.x, y=pt.y) for pt in self.line]),
154+
group_key=group_key)
155+
156+
139157
class NDPolygon(NDBaseObject, ConfidenceMixin):
140158
polygon: List[_Point]
141159

@@ -259,6 +277,31 @@ def from_common(cls, segment):
259277
])
260278

261279

280+
class NDDicomSegment(NDSegment):
281+
keyframes: List[NDDicomLine]
282+
283+
@staticmethod
284+
def lookup_segment_object_type(segment: List) -> "NDDicomObjectType":
285+
"""Used for determining which object type the annotation contains
286+
returns the object type"""
287+
segment_class = type(segment[0].value)
288+
if segment_class == Line:
289+
return NDDicomLine
290+
else:
291+
raise ValueError('DICOM segments only support Line objects')
292+
293+
def to_common(self, name: str, feature_schema_id: Cuid, uuid: str,
294+
segment_index: int, group_key: str):
295+
return [
296+
self.segment_with_uuid(
297+
keyframe.to_common(name=name,
298+
feature_schema_id=feature_schema_id,
299+
segment_index=segment_index,
300+
group_key=group_key), uuid)
301+
for keyframe in self.keyframes
302+
]
303+
304+
262305
class NDSegments(NDBaseObject):
263306
segments: List[NDSegment]
264307

@@ -287,6 +330,36 @@ def from_common(cls, segments: List[VideoObjectAnnotation], data: VideoData,
287330
uuid=extra.get('uuid'))
288331

289332

333+
class NDDicomSegments(NDBaseObject, DicomSupported):
334+
segments: List[NDDicomSegment]
335+
336+
def to_common(self, name: str, feature_schema_id: Cuid):
337+
result = []
338+
for idx, segment in enumerate(self.segments):
339+
result.extend(
340+
NDDicomSegment.to_common(segment,
341+
name=name,
342+
feature_schema_id=feature_schema_id,
343+
segment_index=idx,
344+
uuid=self.uuid,
345+
group_key=self.group_key))
346+
return result
347+
348+
@classmethod
349+
def from_common(cls, segments: List[DICOMObjectAnnotation], data: VideoData,
350+
name: str, feature_schema_id: Cuid, extra: Dict[str, Any],
351+
group_key: str) -> "NDDicomSegments":
352+
353+
segments = [NDDicomSegment.from_common(segment) for segment in segments]
354+
355+
return cls(segments=segments,
356+
dataRow=DataRow(id=data.uid),
357+
name=name,
358+
schema_id=feature_schema_id,
359+
uuid=extra.get('uuid'),
360+
group_key=group_key)
361+
362+
290363
class _URIMask(BaseModel):
291364
instanceURI: str
292365
colorRGB: Tuple[int, int, int]
@@ -460,13 +533,21 @@ def from_common(
460533
obj = cls.lookup_object(annotation)
461534

462535
# if it is video segments
463-
if (obj == NDSegments):
464-
return obj.from_common(
465-
annotation,
466-
data,
467-
name=annotation[0][0].name,
468-
feature_schema_id=annotation[0][0].feature_schema_id,
469-
extra=annotation[0][0].extra)
536+
if (obj == NDSegments or obj == NDDicomSegments):
537+
538+
first_video_annotation = annotation[0][0]
539+
args = dict(
540+
segments=annotation,
541+
data=data,
542+
name=first_video_annotation.name,
543+
feature_schema_id=first_video_annotation.feature_schema_id,
544+
extra=first_video_annotation.extra)
545+
546+
if isinstance(first_video_annotation, DICOMObjectAnnotation):
547+
group_key = first_video_annotation.group_key.value
548+
args.update(dict(group_key=group_key))
549+
550+
return obj.from_common(**args)
470551

471552
subclasses = [
472553
NDSubclassification.from_common(annot)
@@ -483,7 +564,15 @@ def from_common(
483564
def lookup_object(
484565
annotation: Union[ObjectAnnotation, List]) -> "NDObjectType":
485566
if isinstance(annotation, list):
486-
result = NDSegments
567+
try:
568+
first_annotation = annotation[0][0]
569+
except IndexError:
570+
raise ValueError("Annotation list cannot be empty")
571+
572+
if isinstance(first_annotation, DICOMObjectAnnotation):
573+
result = NDDicomSegments
574+
else:
575+
result = NDSegments
487576
else:
488577
result = {
489578
Line: NDLine,
@@ -510,3 +599,4 @@ def lookup_object(
510599
NDEntityType, NDDocumentEntity]
511600

512601
NDFrameObjectType = NDFrameRectangle, NDFramePoint, NDFrameLine
602+
NDDicomObjectType = NDDicomLine

tests/data/serialization/coco/test_coco.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,29 @@
66
COCO_ASSETS_DIR = "tests/data/assets/coco"
77

88

9-
def run_instances():
9+
def run_instances(tmpdir):
1010
instance_json = json.load(open(Path(COCO_ASSETS_DIR, 'instances.json')))
1111
res = COCOConverter.deserialize_instances(instance_json,
1212
Path(COCO_ASSETS_DIR, 'images'))
1313
back = COCOConverter.serialize_instances(
1414
res,
15-
Path('tmp/images_instances'),
15+
Path(tmpdir),
1616
)
1717

1818

19-
def test_rle_objects():
19+
def test_rle_objects(tmpdir):
2020
rle_json = json.load(open(Path(COCO_ASSETS_DIR, 'rle.json')))
2121
res = COCOConverter.deserialize_instances(rle_json,
2222
Path(COCO_ASSETS_DIR, 'images'))
23-
back = COCOConverter.serialize_instances(res, Path('/tmp/images_rle'))
23+
back = COCOConverter.serialize_instances(res, tmpdir)
2424

2525

26-
def test_panoptic():
26+
def test_panoptic(tmpdir):
2727
panoptic_json = json.load(open(Path(COCO_ASSETS_DIR, 'panoptic.json')))
2828
image_dir, mask_dir = [
2929
Path(COCO_ASSETS_DIR, dir_name) for dir_name in ['images', 'masks']
3030
]
3131
res = COCOConverter.deserialize_panoptic(panoptic_json, image_dir, mask_dir)
32-
back = COCOConverter.serialize_panoptic(res, Path('/tmp/images_panoptic'),
33-
Path('/tmp/masks_panoptic'))
32+
back = COCOConverter.serialize_panoptic(res,
33+
Path(f'/{tmpdir}/images_panoptic'),
34+
Path(f'/{tmpdir}/masks_panoptic'))

0 commit comments

Comments
 (0)