From 4ed678588475c99f3a23c43b5dbd96a41b2b5702 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Mon, 27 Jun 2022 19:39:03 +0300 Subject: [PATCH 01/20] [AL-2778][AL-2779] Add name field to annotations --- labelbox/data/serialization/ndjson/base.py | 17 +++++-- .../serialization/ndjson/classification.py | 48 ++++++++++++------- labelbox/data/serialization/ndjson/label.py | 2 +- labelbox/data/serialization/ndjson/objects.py | 43 +++++++++++------ 4 files changed, 73 insertions(+), 37 deletions(-) diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 2561fd005..7b659f4ad 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -1,3 +1,4 @@ +from typing import Optional from uuid import uuid4 from pydantic import BaseModel, validator, Field @@ -32,12 +33,18 @@ class Config: class NDAnnotation(NDJsonBase): - schema_id: Cuid + schema_id: Optional[Cuid] = None + name: Optional[str] = None + + @validator('name', pre=True, always=True) + def validate_name(cls, v, values): + if v is None and 'schema_id' not in values: + raise ValueError("Name is not set. Either set name or schema_id.") @validator('schema_id', pre=True, always=True) - def validate_id(cls, v): - if v is None: + def validate_id(cls, v, values): + if v is None and 'name' not in values: raise ValueError( - "Schema ids are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." + "Schema ids or names are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." ) - return v + return v \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 6a28f09cf..8f8482b67 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -11,13 +11,19 @@ class NDFeature(BaseModel): - schema_id: Cuid + schema_id: Optional[Cuid] = None + name: Optional[str] = None + + @validator('name', pre=True, always=True) + def validate_name(cls, v, values): + if v is None and 'schema_id' not in values: + raise ValueError("Name is not set. Either set name or schema_id.") @validator('schema_id', pre=True, always=True) - def validate_id(cls, v): - if v is None: + def validate_id(cls, v, values): + if v is None and 'name' not in values: raise ValueError( - "Schema ids are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." + "Schema ids or names are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." ) return v @@ -50,9 +56,9 @@ def to_common(self) -> Text: return Text(answer=self.answer) @classmethod - def from_common(cls, text: Text, + def from_common(cls, text: Text, name: str, feature_schema_id: Cuid) -> "NDTextSubclass": - return cls(answer=text.answer, schema_id=feature_schema_id) + return cls(answer=text.answer, name=name, schema_id=feature_schema_id) class NDChecklistSubclass(NDFeature): @@ -60,15 +66,16 @@ class NDChecklistSubclass(NDFeature): def to_common(self) -> Checklist: return Checklist(answer=[ - ClassificationAnswer(feature_schema_id=answer.schema_id) + ClassificationAnswer(name=answer.name, + feature_schema_id=answer.schema_id) for answer in self.answer ]) @classmethod - def from_common(cls, checklist: Checklist, + def from_common(cls, checklist: Checklist, name: str, feature_schema_id: Cuid) -> "NDChecklistSubclass": return cls(answer=[ - NDFeature(schema_id=answer.feature_schema_id) + NDFeature(name=name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], schema_id=feature_schema_id) @@ -85,12 +92,14 @@ class NDRadioSubclass(NDFeature): def to_common(self) -> Radio: return Radio(answer=ClassificationAnswer( - feature_schema_id=self.answer.schema_id)) + name=self.answer.name, feature_schema_id=self.answer.schema_id)) @classmethod - def from_common(cls, radio: Radio, + def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid) -> "NDRadioSubclass": - return cls(answer=NDFeature(schema_id=radio.answer.feature_schema_id), + return cls(answer=NDFeature(name=radio.answer.name, + schema_id=radio.answer.feature_schema_id), + name=name, schema_id=feature_schema_id) @@ -100,12 +109,13 @@ def from_common(cls, radio: Radio, class NDText(NDAnnotation, NDTextSubclass): @classmethod - def from_common(cls, text: Text, feature_schema_id: Cuid, + def from_common(cls, text: Text, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[TextData, ImageData]) -> "NDText": return cls( answer=text.answer, data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), ) @@ -115,14 +125,15 @@ class NDChecklist(NDAnnotation, NDChecklistSubclass, VideoSupported): @classmethod def from_common( - cls, checklist: Checklist, feature_schema_id: Cuid, + cls, checklist: Checklist, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDChecklist": return cls(answer=[ - NDFeature(schema_id=answer.feature_schema_id) + NDFeature(name=name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), frames=extra.get('frames')) @@ -131,11 +142,12 @@ def from_common( class NDRadio(NDAnnotation, NDRadioSubclass, VideoSupported): @classmethod - def from_common(cls, radio: Radio, feature_schema_id: Cuid, + def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDRadio": return cls(answer=NDFeature(schema_id=radio.answer.feature_schema_id), data_row={'id': data.uid}, + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), frames=extra.get('frames')) @@ -159,6 +171,7 @@ def from_common( def to_common( annotation: "NDClassificationType") -> ClassificationAnnotation: return ClassificationAnnotation(value=annotation.to_common(), + name=annotation.name, feature_schema_id=annotation.schema_id) @staticmethod @@ -182,6 +195,7 @@ def to_common( ) -> Union[ClassificationAnnotation, VideoClassificationAnnotation]: common = ClassificationAnnotation( value=annotation.to_common(), + name=annotation.name, feature_schema_id=annotation.schema_id, extra={'uuid': annotation.uuid}) if getattr(annotation, 'frames', None) is None: @@ -204,7 +218,7 @@ def from_common( raise TypeError( f"Unable to convert object to MAL format. `{type(annotation.value)}`" ) - return classify_obj.from_common(annotation.value, + return classify_obj.from_common(annotation.value, annotation.name, annotation.feature_schema_id, annotation.extra, data) diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index 529d711a7..75891e6ce 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -95,7 +95,7 @@ def _create_video_annotations( if isinstance( annot, (VideoClassificationAnnotation, VideoObjectAnnotation)): - video_annotations[annot.feature_schema_id].append(annot) + video_annotations[annot.name or annot.schema_id].append(annot) for annotation_group in video_annotations.values(): consecutive_frames = cls._get_consecutive_frames( diff --git a/labelbox/data/serialization/ndjson/objects.py b/labelbox/data/serialization/ndjson/objects.py index ccf1e69f8..f2cf01281 100644 --- a/labelbox/data/serialization/ndjson/objects.py +++ b/labelbox/data/serialization/ndjson/objects.py @@ -47,7 +47,7 @@ def to_common(self) -> Point: @classmethod def from_common(cls, point: Point, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDPoint": return cls(point={ @@ -55,6 +55,7 @@ def from_common(cls, point: Point, 'y': point.y }, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -68,7 +69,7 @@ def to_common(self) -> Line: @classmethod def from_common(cls, line: Line, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDLine": return cls(line=[{ @@ -76,6 +77,7 @@ def from_common(cls, line: Line, 'y': pt.y } for pt in line.points], dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -89,7 +91,7 @@ def to_common(self) -> Polygon: @classmethod def from_common(cls, polygon: Polygon, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDPolygon": return cls(polygon=[{ @@ -97,6 +99,7 @@ def from_common(cls, polygon: Polygon, 'y': pt.y } for pt in polygon.points], dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -112,7 +115,7 @@ def to_common(self) -> Rectangle: @classmethod def from_common(cls, rectangle: Rectangle, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDRectangle": return cls(bbox=Bbox(top=rectangle.start.y, @@ -120,6 +123,7 @@ def from_common(cls, rectangle: Rectangle, height=rectangle.end.y - rectangle.start.y, width=rectangle.end.x - rectangle.start.x), dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -128,10 +132,12 @@ def from_common(cls, rectangle: Rectangle, class NDFrameRectangle(VideoSupported): bbox: Bbox - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation( frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Rectangle(start=Point(x=self.bbox.left, y=self.bbox.top), end=Point(x=self.bbox.left + self.bbox.width, @@ -156,9 +162,10 @@ def lookup_segment_object_type(segment: List) -> "NDFrameObjectType": result = {Rectangle: NDFrameRectangle}.get(type(segment[0].value)) return result - def to_common(self, feature_schema_id: Cuid): + def to_common(self, name: str, feature_schema_id: Cuid): return [ - keyframe.to_common(feature_schema_id) for keyframe in self.keyframes + keyframe.to_common(name=name, feature_schema_id=feature_schema_id) + for keyframe in self.keyframes ] @classmethod @@ -175,21 +182,25 @@ def from_common(cls, segment): class NDSegments(NDBaseObject): segments: List[NDSegment] - def to_common(self, feature_schema_id: Cuid): + def to_common(self, name: str, feature_schema_id: Cuid): result = [] for segment in self.segments: - result.extend(NDSegment.to_common(segment, feature_schema_id)) + result.extend( + NDSegment.to_common(segment, + name=name, + feature_schema_id=feature_schema_id)) return result @classmethod def from_common(cls, segments: List[VideoObjectAnnotation], data: VideoData, - feature_schema_id: Cuid, extra: Dict[str, - Any]) -> "NDSegments": + name: str, feature_schema_id: Cuid, + extra: Dict[str, Any]) -> "NDSegments": segments = [NDSegment.from_common(segment) for segment in segments] return cls(segments=segments, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid')) @@ -222,7 +233,7 @@ def to_common(self) -> Mask: @classmethod def from_common(cls, mask: Mask, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDMask": @@ -237,6 +248,7 @@ def from_common(cls, mask: Mask, return cls(mask=lbv1_mask, dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -255,7 +267,7 @@ def to_common(self) -> TextEntity: @classmethod def from_common(cls, text_entity: TextEntity, - classifications: List[ClassificationAnnotation], + classifications: List[ClassificationAnnotation], name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[ImageData, TextData]) -> "NDTextEntity": return cls(location=Location( @@ -263,6 +275,7 @@ def from_common(cls, text_entity: TextEntity, end=text_entity.end, ), dataRow=DataRow(id=data.uid), + name=name, schema_id=feature_schema_id, uuid=extra.get('uuid'), classifications=classifications) @@ -278,6 +291,7 @@ def to_common(annotation: "NDObjectType") -> ObjectAnnotation: for annot in annotation.classifications ] return ObjectAnnotation(value=common_annotation, + name=annotation.name, feature_schema_id=annotation.schema_id, classifications=classifications, extra={'uuid': annotation.uuid}) @@ -295,6 +309,7 @@ def from_common( return obj.from_common( annotation, data, + name=annotation[0][0].name, feature_schema_id=annotation[0][0].feature_schema_id, extra=annotation[0][0].extra) @@ -302,7 +317,7 @@ def from_common( NDSubclassification.from_common(annot) for annot in annotation.classifications ] - return obj.from_common(annotation.value, subclasses, + return obj.from_common(annotation.value, subclasses, annotation.name, annotation.feature_schema_id, annotation.extra, data) From 99f6ff463cc1da24725237c06e19027cdc4c3061 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Tue, 28 Jun 2022 19:48:07 +0300 Subject: [PATCH 02/20] exclude none values when serializing --- labelbox/data/serialization/ndjson/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labelbox/data/serialization/ndjson/converter.py b/labelbox/data/serialization/ndjson/converter.py index fc4b00808..0c4f0097f 100644 --- a/labelbox/data/serialization/ndjson/converter.py +++ b/labelbox/data/serialization/ndjson/converter.py @@ -39,4 +39,4 @@ def serialize( A generator for accessing the ndjson representation of the data """ for example in NDLabel.from_common(labels): - yield example.dict(by_alias=True) + yield example.dict(by_alias=True, exclude_none=True) From 09b5d45826f55df1277b4a1d4f0a21ad9d7c6c60 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Tue, 28 Jun 2022 19:58:44 +0300 Subject: [PATCH 03/20] remove only necessary fields --- labelbox/data/serialization/ndjson/converter.py | 7 ++++++- labelbox/data/serialization/ndjson/label.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/labelbox/data/serialization/ndjson/converter.py b/labelbox/data/serialization/ndjson/converter.py index 0c4f0097f..25177daf5 100644 --- a/labelbox/data/serialization/ndjson/converter.py +++ b/labelbox/data/serialization/ndjson/converter.py @@ -39,4 +39,9 @@ def serialize( A generator for accessing the ndjson representation of the data """ for example in NDLabel.from_common(labels): - yield example.dict(by_alias=True, exclude_none=True) + res = example.dict(by_alias=True) + if res.schema_id is None: + res.pop('schema_id') + if res.name is None: + res.pop('name') + yield res diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index 75891e6ce..dca47bd98 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -95,7 +95,7 @@ def _create_video_annotations( if isinstance( annot, (VideoClassificationAnnotation, VideoObjectAnnotation)): - video_annotations[annot.name or annot.schema_id].append(annot) + video_annotations[annot.schema_id or annot.name].append(annot) for annotation_group in video_annotations.values(): consecutive_frames = cls._get_consecutive_frames( From d841ef49aa2b32287d247c97097df460c9d76f02 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Tue, 28 Jun 2022 20:13:35 +0300 Subject: [PATCH 04/20] override dicts --- labelbox/data/annotation_types/feature.py | 8 ++++++++ labelbox/data/serialization/ndjson/base.py | 10 +++++++++- labelbox/data/serialization/ndjson/classification.py | 8 ++++++++ labelbox/data/serialization/ndjson/converter.py | 7 +------ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index a6b08b9b5..e8f1d0c90 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -24,3 +24,11 @@ def must_set_one(cls, values): "Must set either feature_schema_id or name for all feature schemas" ) return values + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if res.name is None: + res.pop('name') + if res.feature_schema_id is None: + res.pop('feature_schema_id') + return res diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 7b659f4ad..4dcf8beee 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -47,4 +47,12 @@ def validate_id(cls, v, values): raise ValueError( "Schema ids or names are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." ) - return v \ No newline at end of file + return v + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if res.name is None: + res.pop('name') + if res.schema_id is None: + res.pop('schema_id') + return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 8f8482b67..d963748f8 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -27,6 +27,14 @@ def validate_id(cls, v, values): ) return v + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if res.name is None: + res.pop('name') + if res.schema_id is None: + res.pop('schema_id') + return res + class Config: allow_population_by_field_name = True alias_generator = camel_case diff --git a/labelbox/data/serialization/ndjson/converter.py b/labelbox/data/serialization/ndjson/converter.py index 25177daf5..fc4b00808 100644 --- a/labelbox/data/serialization/ndjson/converter.py +++ b/labelbox/data/serialization/ndjson/converter.py @@ -39,9 +39,4 @@ def serialize( A generator for accessing the ndjson representation of the data """ for example in NDLabel.from_common(labels): - res = example.dict(by_alias=True) - if res.schema_id is None: - res.pop('schema_id') - if res.name is None: - res.pop('name') - yield res + yield example.dict(by_alias=True) From ebd97c1400df3b29ec17d6df43dda1e87536cb08 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 29 Jun 2022 19:03:04 +0300 Subject: [PATCH 05/20] change object access --- labelbox/data/annotation_types/feature.py | 4 ++-- labelbox/data/serialization/ndjson/base.py | 4 ++-- labelbox/data/serialization/ndjson/classification.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index e8f1d0c90..4f78dcc0f 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -27,8 +27,8 @@ def must_set_one(cls, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res.name is None: + if res['name'] is None: res.pop('name') - if res.feature_schema_id is None: + if res['feature_schema_id'] is None: res.pop('feature_schema_id') return res diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 4dcf8beee..07a3d90d5 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -51,8 +51,8 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res.name is None: + if res['name'] is None: res.pop('name') - if res.schema_id is None: + if res['schema_id'] is None: res.pop('schema_id') return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index d963748f8..1f3cb532b 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -29,9 +29,9 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res.name is None: + if res['name'] is None: res.pop('name') - if res.schema_id is None: + if res['schema_id'] is None: res.pop('schema_id') return res From 25db9d4279c3f95f3ee41f8ddfe3e9d30b2ce026 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 29 Jun 2022 19:14:37 +0300 Subject: [PATCH 06/20] update tests --- .../classification/test_classification.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/data/annotation_types/classification/test_classification.py b/tests/data/annotation_types/classification/test_classification.py index 5f6924f3b..39f566dcb 100644 --- a/tests/data/annotation_types/classification/test_classification.py +++ b/tests/data/annotation_types/classification/test_classification.py @@ -45,7 +45,6 @@ def test_subclass(): name=name) assert classification.dict() == { 'name': name, - 'feature_schema_id': None, 'extra': {}, 'value': { 'answer': answer @@ -130,7 +129,6 @@ def test_checklist(): assert classification.dict() == { 'answer': [{ 'name': answer.name, - 'feature_schema_id': None, 'extra': {} }] } @@ -165,13 +163,7 @@ def test_dropdown(): with pytest.raises(ValidationError): classification = Dropdown(answer=answer) classification = Dropdown(answer=[answer]) - assert classification.dict() == { - 'answer': [{ - 'name': '1', - 'feature_schema_id': None, - 'extra': {} - }] - } + assert classification.dict() == {'answer': [{'name': '1', 'extra': {}}]} classification = ClassificationAnnotation( value=Dropdown(answer=[answer]), feature_schema_id=feature_schema_id, @@ -183,7 +175,6 @@ def test_dropdown(): 'value': { 'answer': [{ 'name': answer.name, - 'feature_schema_id': None, 'extra': {} }] } From 69cf86f280e854e88596f50bd7fe035bcd794051 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 29 Jun 2022 19:24:09 +0300 Subject: [PATCH 07/20] remove more None --- .../annotation_types/classification/test_classification.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/data/annotation_types/classification/test_classification.py b/tests/data/annotation_types/classification/test_classification.py index 39f566dcb..e571c3a46 100644 --- a/tests/data/annotation_types/classification/test_classification.py +++ b/tests/data/annotation_types/classification/test_classification.py @@ -92,7 +92,6 @@ def test_radio(): assert classification.dict() == { 'answer': { 'name': answer.name, - 'feature_schema_id': None, 'extra': {} } } @@ -107,7 +106,6 @@ def test_radio(): 'value': { 'answer': { 'name': answer.name, - 'feature_schema_id': None, 'extra': {} } } @@ -144,7 +142,6 @@ def test_checklist(): 'value': { 'answer': [{ 'name': answer.name, - 'feature_schema_id': None, 'extra': {} }] }, From a28ff68a11daacef10ce3145d3df77d96f2783e0 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 29 Jun 2022 19:36:21 +0300 Subject: [PATCH 08/20] use camel case --- labelbox/data/annotation_types/feature.py | 4 ++-- labelbox/data/serialization/ndjson/base.py | 6 +++--- labelbox/data/serialization/ndjson/classification.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index 4f78dcc0f..43099fc6b 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -27,8 +27,8 @@ def must_set_one(cls, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if self.name is None: res.pop('name') - if res['feature_schema_id'] is None: + if self.feature_schema_id is None: res.pop('feature_schema_id') return res diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 07a3d90d5..d42ec311b 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -51,8 +51,8 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if self.name is None: res.pop('name') - if res['schema_id'] is None: - res.pop('schema_id') + if self.schema_id is None: + res.pop('schemaId') return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 1f3cb532b..1fe2ef392 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -29,10 +29,10 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if self.name is None: res.pop('name') - if res['schema_id'] is None: - res.pop('schema_id') + if self.schema_id is None: + res.pop('schemaId') return res class Config: From bf4b3e4f6d76b4b0be71d3d5521c621924eb97b3 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 29 Jun 2022 20:07:40 +0300 Subject: [PATCH 09/20] remove from dict --- labelbox/data/annotation_types/feature.py | 4 ++-- labelbox/data/serialization/ndjson/base.py | 4 ++-- labelbox/data/serialization/ndjson/classification.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index 43099fc6b..4f78dcc0f 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -27,8 +27,8 @@ def must_set_one(cls, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if self.name is None: + if res['name'] is None: res.pop('name') - if self.feature_schema_id is None: + if res['feature_schema_id'] is None: res.pop('feature_schema_id') return res diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index d42ec311b..13323063f 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -51,8 +51,8 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if self.name is None: + if res['name'] is None: res.pop('name') - if self.schema_id is None: + if res['schemaId'] is None: res.pop('schemaId') return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 1fe2ef392..7aa5d228f 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -29,9 +29,9 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if self.name is None: + if res['name'] is None: res.pop('name') - if self.schema_id is None: + if res['schemaId'] is None: res.pop('schemaId') return res From f9a93d9f1e973e1978a71fb178fba1884dcaad4c Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Fri, 1 Jul 2022 20:36:31 +0300 Subject: [PATCH 10/20] updates --- labelbox/data/annotation_types/feature.py | 9 +++------ labelbox/data/annotation_types/label.py | 4 ++++ labelbox/data/serialization/ndjson/base.py | 8 +++----- .../data/serialization/ndjson/classification.py | 10 ++++++---- labelbox/data/serialization/ndjson/label.py | 7 +++++-- .../classification/test_classification.py | 16 ++++++++++++++-- 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index 4f78dcc0f..b0a37bb04 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -10,9 +10,6 @@ class FeatureSchema(BaseModel): Class that represents a feature schema. Could be a annotation, a subclass, or an option. Schema ids might not be known when constructing these objects so both a name and schema id are valid. - - Use `LabelList.assign_feature_schema_ids` or `LabelGenerator.assign_feature_schema_ids` - to retroactively add schema ids by looking them up from the names. """ name: Optional[str] = None feature_schema_id: Optional[Cuid] = None @@ -27,8 +24,8 @@ def must_set_one(cls, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if 'name' in res and res['name'] is None: res.pop('name') - if res['feature_schema_id'] is None: - res.pop('feature_schema_id') + if 'featureSchemaId' in res and res.featureSchemaId is None: + res.pop('featureSchemaId') return res diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index 8ce4aed25..fd89c0111 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -1,5 +1,6 @@ from collections import defaultdict from typing import Any, Callable, Dict, List, Union, Optional +import warnings from pydantic import BaseModel, validator @@ -136,6 +137,9 @@ def assign_feature_schema_ids( ontology_builder: The ontology that matches the feature names assigned to objects in this dataset Returns: Label. useful for chaining these modifying functions + + Warning: assign_feature_schema_ids is now obsolete, you can + now use names directly without having to lookup schema_ids. """ tool_lookup, classification_lookup = get_feature_schema_lookup( ontology_builder) diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 13323063f..451ee1114 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -44,15 +44,13 @@ def validate_name(cls, v, values): @validator('schema_id', pre=True, always=True) def validate_id(cls, v, values): if v is None and 'name' not in values: - raise ValueError( - "Schema ids or names are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." - ) + raise ValueError("Schema id or name are not set. Set either one.") return v def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if 'name' in res and res['name'] is None: res.pop('name') - if res['schemaId'] is None: + if 'schemaId' in res and res['schemaId'] is None: res.pop('schemaId') return res \ No newline at end of file diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 7aa5d228f..ab6c6739e 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -1,3 +1,4 @@ +from tkinter import N from typing import Any, Dict, List, Union, Optional from pydantic import BaseModel, Field, validator @@ -29,9 +30,9 @@ def validate_id(cls, v, values): def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) - if res['name'] is None: + if 'name' in res and res['name'] is None: res.pop('name') - if res['schemaId'] is None: + if 'schemaId' in res and res['schemaId'] is None: res.pop('schemaId') return res @@ -153,7 +154,8 @@ class NDRadio(NDAnnotation, NDRadioSubclass, VideoSupported): def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid, extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDRadio": - return cls(answer=NDFeature(schema_id=radio.answer.feature_schema_id), + return cls(answer=NDFeature(name=radio.answer.name, + schema_id=radio.answer.feature_schema_id), data_row={'id': data.uid}, name=name, schema_id=feature_schema_id, @@ -172,7 +174,7 @@ def from_common( raise TypeError( f"Unable to convert object to MAL format. `{type(annotation.value)}`" ) - return classify_obj.from_common(annotation.value, + return classify_obj.from_common(annotation.value, annotation.name, annotation.feature_schema_id) @staticmethod diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index dca47bd98..63090da67 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -50,7 +50,8 @@ def _generate_annotations( for annotation in annotations: if isinstance(annotation, NDSegments): annots.extend( - NDSegments.to_common(annotation, annotation.schema_id)) + NDSegments.to_common(annotation, annotation.name, + annotation.schema_id)) elif isinstance(annotation, NDObjectType.__args__): annots.append(NDObject.to_common(annotation)) @@ -92,10 +93,12 @@ def _create_video_annotations( video_annotations = defaultdict(list) for annot in label.annotations: + # print(dict(annot)) if isinstance( annot, (VideoClassificationAnnotation, VideoObjectAnnotation)): - video_annotations[annot.schema_id or annot.name].append(annot) + video_annotations[annot.feature_schema_id or + annot.name].append(annot) for annotation_group in video_annotations.values(): consecutive_frames = cls._get_consecutive_frames( diff --git a/tests/data/annotation_types/classification/test_classification.py b/tests/data/annotation_types/classification/test_classification.py index e571c3a46..874102c66 100644 --- a/tests/data/annotation_types/classification/test_classification.py +++ b/tests/data/annotation_types/classification/test_classification.py @@ -43,8 +43,9 @@ def test_subclass(): classification = ClassificationAnnotation(value=Text(answer=answer)) classification = ClassificationAnnotation(value=Text(answer=answer), name=name) - assert classification.dict() == { + assert classification.dict(by_alias=True) == { 'name': name, + 'feature_schema_id': None, 'extra': {}, 'value': { 'answer': answer @@ -92,6 +93,7 @@ def test_radio(): assert classification.dict() == { 'answer': { 'name': answer.name, + 'feature_schema_id': None, 'extra': {} } } @@ -106,6 +108,7 @@ def test_radio(): 'value': { 'answer': { 'name': answer.name, + 'feature_schema_id': None, 'extra': {} } } @@ -127,6 +130,7 @@ def test_checklist(): assert classification.dict() == { 'answer': [{ 'name': answer.name, + 'feature_schema_id': None, 'extra': {} }] } @@ -142,6 +146,7 @@ def test_checklist(): 'value': { 'answer': [{ 'name': answer.name, + 'feature_schema_id': None, 'extra': {} }] }, @@ -160,7 +165,13 @@ def test_dropdown(): with pytest.raises(ValidationError): classification = Dropdown(answer=answer) classification = Dropdown(answer=[answer]) - assert classification.dict() == {'answer': [{'name': '1', 'extra': {}}]} + assert classification.dict() == { + 'answer': [{ + 'name': '1', + 'feature_schema_id': None, + 'extra': {} + }] + } classification = ClassificationAnnotation( value=Dropdown(answer=[answer]), feature_schema_id=feature_schema_id, @@ -172,6 +183,7 @@ def test_dropdown(): 'value': { 'answer': [{ 'name': answer.name, + 'feature_schema_id': None, 'extra': {} }] } From bee42bb37e7a3ea245aff0e427c5832ed0296018 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Fri, 1 Jul 2022 21:35:29 +0300 Subject: [PATCH 11/20] no by_alias --- .../data/annotation_types/classification/test_classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/annotation_types/classification/test_classification.py b/tests/data/annotation_types/classification/test_classification.py index 874102c66..5f6924f3b 100644 --- a/tests/data/annotation_types/classification/test_classification.py +++ b/tests/data/annotation_types/classification/test_classification.py @@ -43,7 +43,7 @@ def test_subclass(): classification = ClassificationAnnotation(value=Text(answer=answer)) classification = ClassificationAnnotation(value=Text(answer=answer), name=name) - assert classification.dict(by_alias=True) == { + assert classification.dict() == { 'name': name, 'feature_schema_id': None, 'extra': {}, From 7f948f9ab7070e75f5dc47fd3f3999316265f720 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Fri, 1 Jul 2022 22:20:21 +0300 Subject: [PATCH 12/20] update error --- labelbox/data/serialization/ndjson/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index ab6c6739e..a7581565a 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -24,7 +24,7 @@ def validate_name(cls, v, values): def validate_id(cls, v, values): if v is None and 'name' not in values: raise ValueError( - "Schema ids or names are not set. Use `LabelGenerator.assign_feature_schema_ids`, `LabelList.assign_feature_schema_ids`, or `Label.assign_feature_schema_ids`." + "Schema ids or names are not set. Either set name or schema_id.`." ) return v From 1aae84c0dc42abe5d1d8826d9bef642c630ea9d7 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Fri, 1 Jul 2022 22:52:52 +0300 Subject: [PATCH 13/20] pr comments --- labelbox/data/annotation_types/feature.py | 2 +- labelbox/data/serialization/ndjson/classification.py | 1 - labelbox/data/serialization/ndjson/label.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/labelbox/data/annotation_types/feature.py b/labelbox/data/annotation_types/feature.py index b0a37bb04..f3ae8eb8f 100644 --- a/labelbox/data/annotation_types/feature.py +++ b/labelbox/data/annotation_types/feature.py @@ -26,6 +26,6 @@ def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) if 'name' in res and res['name'] is None: res.pop('name') - if 'featureSchemaId' in res and res.featureSchemaId is None: + if 'featureSchemaId' in res and res['featureSchemaId'] is None: res.pop('featureSchemaId') return res diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index a7581565a..0f6d6751f 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -1,4 +1,3 @@ -from tkinter import N from typing import Any, Dict, List, Union, Optional from pydantic import BaseModel, Field, validator diff --git a/labelbox/data/serialization/ndjson/label.py b/labelbox/data/serialization/ndjson/label.py index 63090da67..e7b36a1dc 100644 --- a/labelbox/data/serialization/ndjson/label.py +++ b/labelbox/data/serialization/ndjson/label.py @@ -93,7 +93,6 @@ def _create_video_annotations( video_annotations = defaultdict(list) for annot in label.annotations: - # print(dict(annot)) if isinstance( annot, (VideoClassificationAnnotation, VideoObjectAnnotation)): From 4ac94f3bb698eea6a60fd48da9c837afa34c6d62 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Mon, 11 Jul 2022 19:44:02 +0300 Subject: [PATCH 14/20] fix validation, add tests, add comments --- labelbox/data/annotation_types/collection.py | 4 +- labelbox/data/annotation_types/label.py | 1 - labelbox/data/serialization/ndjson/base.py | 3 +- .../serialization/ndjson/classification.py | 3 +- labelbox/schema/bulk_import_request.py | 159 +++++++++++++----- .../integration/annotation_import/conftest.py | 8 +- .../test_ndjson_validation.py | 19 +++ 7 files changed, 151 insertions(+), 46 deletions(-) diff --git a/labelbox/data/annotation_types/collection.py b/labelbox/data/annotation_types/collection.py index e5c60a15b..8272efc0d 100644 --- a/labelbox/data/annotation_types/collection.py +++ b/labelbox/data/annotation_types/collection.py @@ -34,12 +34,14 @@ def assign_feature_schema_ids( self, ontology_builder: "ontology.OntologyBuilder") -> "LabelList": """ Adds schema ids to all FeatureSchema objects in the Labels. - This is necessary for MAL. Args: ontology_builder: The ontology that matches the feature names assigned to objects in this LabelList Returns: LabelList. useful for chaining these modifying functions + + Warning: assign_feature_schema_ids is now obsolete, you can + now use names directly without having to lookup schema_ids. """ for label in self._data: label.assign_feature_schema_ids(ontology_builder) diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index fd89c0111..ff147aa8c 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -131,7 +131,6 @@ def assign_feature_schema_ids( self, ontology_builder: ontology.OntologyBuilder) -> "Label": """ Adds schema ids to all FeatureSchema objects in the Labels. - This is necessary for MAL. Args: ontology_builder: The ontology that matches the feature names assigned to objects in this dataset diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 451ee1114..410ae1340 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -39,7 +39,8 @@ class NDAnnotation(NDJsonBase): @validator('name', pre=True, always=True) def validate_name(cls, v, values): if v is None and 'schema_id' not in values: - raise ValueError("Name is not set. Either set name or schema_id.") + raise ValueError( + "Name and schema_id are not set. Either set name or schema_id.") @validator('schema_id', pre=True, always=True) def validate_id(cls, v, values): diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 0f6d6751f..17beefbdb 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -17,7 +17,8 @@ class NDFeature(BaseModel): @validator('name', pre=True, always=True) def validate_name(cls, v, values): if v is None and 'schema_id' not in values: - raise ValueError("Name is not set. Either set name or schema_id.") + raise ValueError( + "Name and schema_id are not set. Either set name or schema_id.") @validator('schema_id', pre=True, always=True) def validate_id(cls, v, values): diff --git a/labelbox/schema/bulk_import_request.py b/labelbox/schema/bulk_import_request.py index c0093473f..5a12c2a85 100644 --- a/labelbox/schema/bulk_import_request.py +++ b/labelbox/schema/bulk_import_request.py @@ -9,12 +9,14 @@ import backoff import ndjson import requests -from pydantic import BaseModel, validator +from pydantic import BaseModel, root_validator, validator from typing_extensions import Literal from typing import (Any, List, Optional, BinaryIO, Dict, Iterable, Tuple, Union, Type, Set, TYPE_CHECKING) from labelbox import exceptions as lb_exceptions +from labelbox.data.annotation_types.types import Cuid +from labelbox.data.ontology import get_feature_schema_lookup from labelbox.orm.model import Entity from labelbox import utils from labelbox.orm import query @@ -408,12 +410,14 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]], MALValidationError: Raise for invalid NDJson UuidError: Duplicate UUID in upload """ - feature_schemas = get_mal_schemas(project.ontology()) + feature_schemas_by_id, feature_schemas_by_name = get_mal_schemas( + project.ontology()) uids: Set[str] = set() for idx, line in enumerate(lines): try: annotation = NDAnnotation(**line) - annotation.validate_instance(feature_schemas) + annotation.validate_instance(feature_schemas_by_id, + feature_schemas_by_name) uuid = str(annotation.uuid) if uuid in uids: raise lb_exceptions.UuidError( @@ -437,14 +441,18 @@ def parse_classification(tool): dict """ if tool['type'] in ['radio', 'checklist']: + option_schema_ids = [r['featureSchemaId'] for r in tool['options']] + option_names = [r['value'] for r in tool['options']] return { 'tool': tool['type'], 'featureSchemaId': tool['featureSchemaId'], - 'options': [r['featureSchemaId'] for r in tool['options']] + 'name': tool['name'], + 'options': [*option_schema_ids, *option_names] } elif tool['type'] == 'text': return { 'tool': tool['type'], + 'name': tool['name'], 'featureSchemaId': tool['featureSchemaId'] } @@ -456,24 +464,37 @@ def get_mal_schemas(ontology): Args: ontology (Ontology) Returns: - Dict : Useful for looking up a tool from a given feature schema id + Dict, Dict : Useful for looking up a tool from a given feature schema id or name """ - valid_feature_schemas = {} + valid_feature_schemas_by_schema_id = {} + valid_feature_schemas_by_name = {} for tool in ontology.normalized['tools']: classifications = [ parse_classification(classification_tool) for classification_tool in tool['classifications'] ] - classifications = {v['featureSchemaId']: v for v in classifications} - valid_feature_schemas[tool['featureSchemaId']] = { + classifications_by_schema_id = { + v['featureSchemaId']: v for v in classifications + } + classifications_by_name = {v['name']: v for v in classifications} + valid_feature_schemas_by_schema_id[tool['featureSchemaId']] = { + 'tool': tool['tool'], + 'classificationsBySchemaId': classifications_by_schema_id, + 'classificationsByName': classifications_by_name, + 'name': tool['name'] + } + valid_feature_schemas_by_name[tool['name']] = { 'tool': tool['tool'], - 'classifications': classifications + 'classificationsBySchemaId': classifications_by_schema_id, + 'classificationsByName': classifications_by_name, + 'name': tool['name'] } for tool in ontology.normalized['classifications']: - valid_feature_schemas[tool['featureSchemaId']] = parse_classification( - tool) - return valid_feature_schemas + valid_feature_schemas_by_schema_id[ + tool['featureSchemaId']] = parse_classification(tool) + valid_feature_schemas_by_name[tool['name']] = parse_classification(tool) + return valid_feature_schemas_by_schema_id, valid_feature_schemas_by_name LabelboxID: str = pydantic.Field(..., min_length=25, max_length=25) @@ -585,7 +606,15 @@ class DataRow(BaseModel): class NDFeatureSchema(BaseModel): - schemaId: str = LabelboxID + schemaId: Optional[Cuid] = None + name: Optional[str] = None + + @root_validator + def must_set_one(cls, values): + if values['schemaId'] is None and values['name'] is None: + raise ValueError( + "Must set either schemaId or name for all feature schemas") + return values class NDBase(NDFeatureSchema): @@ -593,19 +622,36 @@ class NDBase(NDFeatureSchema): uuid: UUID dataRow: DataRow - def validate_feature_schemas(self, valid_feature_schemas): - if self.schemaId not in valid_feature_schemas: - raise ValueError( - f"schema id {self.schemaId} is not valid for the provided project's ontology." - ) + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + if self.name: + if self.name not in valid_feature_schemas_by_name: + raise ValueError( + f"name {self.name} is not valid for the provided project's ontology." + ) - if self.ontology_type != valid_feature_schemas[self.schemaId]['tool']: - raise ValueError( - f"Schema id {self.schemaId} does not map to the assigned tool {valid_feature_schemas[self.schemaId]['tool']}" - ) + if self.ontology_type != valid_feature_schemas_by_name[ + self.name]['tool']: + raise ValueError( + f"Name {self.name} does not map to the assigned tool {valid_feature_schemas_by_name[self.name]['tool']}" + ) + + if self.schemaId: + if self.schemaId not in valid_feature_schemas_by_id: + raise ValueError( + f"schema id {self.schemaId} is not valid for the provided project's ontology." + ) - def validate_instance(self, valid_feature_schemas): - self.validate_feature_schemas(valid_feature_schemas) + if self.ontology_type != valid_feature_schemas_by_id[ + self.schemaId]['tool']: + raise ValueError( + f"Schema id {self.schemaId} does not map to the assigned tool {valid_feature_schemas_by_id[self.schemaId]['tool']}" + ) + + def validate_instance(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + self.validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) class Config: #Users shouldn't to add extra data to the payload @@ -629,9 +675,20 @@ class NDText(NDBase): #No feature schema to check +class NDAnswer(BaseModel): + schemaId: Optional[Cuid] = None + value: Optional[str] = None + + @root_validator + def must_set_one(cls, values): + if values['schemaId'] is None and values['value'] is None: + raise ValueError("Must set either schemaId or value for answers") + return values + + class NDChecklist(VideoSupported, NDBase): ontology_type: Literal["checklist"] = "checklist" - answers: List[NDFeatureSchema] = pydantic.Field(determinant=True) + answers: List[NDAnswer] = pydantic.Field(determinant=True) @validator('answers', pre=True) def validate_answers(cls, value, field): @@ -640,17 +697,23 @@ def validate_answers(cls, value, field): raise ValueError("Checklist answers should not be empty") return value - def validate_feature_schemas(self, valid_feature_schemas): + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): #Test top level feature schema for this tool - super(NDChecklist, self).validate_feature_schemas(valid_feature_schemas) + super(NDChecklist, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) #Test the feature schemas provided to the answer field - if len(set([answer.schemaId for answer in self.answers])) != len( - self.answers): + if len(set([answer.value or answer.schemaId for answer in self.answers + ])) != len(self.answers): raise ValueError( f"Duplicated featureSchema found for checklist {self.uuid}") for answer in self.answers: - options = valid_feature_schemas[self.schemaId]['options'] - if answer.schemaId not in options: + options = valid_feature_schemas_by_name[ + self. + name]['options'] if self.name else valid_feature_schemas_by_id[ + self.schemaId]['options'] + if answer.value not in options and answer.schemaId not in options: raise ValueError( f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {answer}" ) @@ -658,14 +721,19 @@ def validate_feature_schemas(self, valid_feature_schemas): class NDRadio(VideoSupported, NDBase): ontology_type: Literal["radio"] = "radio" - answer: NDFeatureSchema = pydantic.Field(determinant=True) - - def validate_feature_schemas(self, valid_feature_schemas): - super(NDRadio, self).validate_feature_schemas(valid_feature_schemas) - options = valid_feature_schemas[self.schemaId]['options'] - if self.answer.schemaId not in options: + answer: NDAnswer = pydantic.Field(determinant=True) + + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + super(NDRadio, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) + options = valid_feature_schemas_by_name[ + self.name]['options'] if self.name else valid_feature_schemas_by_id[ + self.schemaId]['options'] + if self.answer.value not in options and self.answer.schemaId not in options: raise ValueError( - f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.schemaId}" + f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.value or self.answer.schemaId}" ) @@ -684,11 +752,20 @@ class NDBaseTool(NDBase): classifications: List[NDClassification] = [] #This is indepdent of our problem - def validate_feature_schemas(self, valid_feature_schemas): - super(NDBaseTool, self).validate_feature_schemas(valid_feature_schemas) + def validate_feature_schemas(self, valid_feature_schemas_by_id, + valid_feature_schemas_by_name): + super(NDBaseTool, + self).validate_feature_schemas(valid_feature_schemas_by_id, + valid_feature_schemas_by_name) for classification in self.classifications: classification.validate_feature_schemas( - valid_feature_schemas[self.schemaId]['classifications']) + valid_feature_schemas_by_name[ + self.name]['classificationsBySchemaId'] + if self.name else valid_feature_schemas_by_id[self.schemaId] + ['classificationsBySchemaId'], valid_feature_schemas_by_name[ + self.name]['classificationsByName'] + if self.name else valid_feature_schemas_by_id[ + self.schemaId]['classificationsByName']) @validator('classifications', pre=True) def validate_subclasses(cls, value, field): diff --git a/tests/integration/annotation_import/conftest.py b/tests/integration/annotation_import/conftest.py index 88d727b31..b50c04716 100644 --- a/tests/integration/annotation_import/conftest.py +++ b/tests/integration/annotation_import/conftest.py @@ -135,6 +135,7 @@ def prediction_id_mapping(configured_project): result[tool_type] = { "uuid": str(uuid.uuid4()), "schemaId": tool['featureSchemaId'], + "name": tool['name'], "dataRow": { "id": configured_project.data_row_ids[idx], }, @@ -178,10 +179,15 @@ def rectangle_inference(prediction_id_mapping): 'classifications': [{ "schemaId": rectangle['tool']['classifications'][0]['featureSchemaId'], + "name": + rectangle['tool']['classifications'][0]['name'], "answer": { "schemaId": rectangle['tool']['classifications'][0]['options'][0] - ['featureSchemaId'] + ['featureSchemaId'], + "value": + rectangle['tool']['classifications'][0]['options'][0] + ['value'] } }] }) diff --git a/tests/integration/annotation_import/test_ndjson_validation.py b/tests/integration/annotation_import/test_ndjson_validation.py index e490a36a2..355c4909e 100644 --- a/tests/integration/annotation_import/test_ndjson_validation.py +++ b/tests/integration/annotation_import/test_ndjson_validation.py @@ -82,6 +82,10 @@ def test_invalid_checklist_item(checklist_inference, configured_project): with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project) + pred['answers'] = [{"name": "asdfg"}] + with pytest.raises(MALValidationError): + _validate_ndjson([pred], configured_project) + pred['answers'] = [{"schemaId": "1232132132"}] with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project) @@ -177,10 +181,25 @@ def test_invalid_feature_schema(configured_project, rectangle_inference): _validate_ndjson([pred], configured_project) +def test_name_only_feature_schema(configured_project, rectangle_inference): + #Trying to upload a polygon and rectangle at the same time + pred = rectangle_inference.copy() + del pred['schemaId'] + _validate_ndjson([pred], configured_project) + + +def test_schema_id_only_feature_schema(configured_project, rectangle_inference): + #Trying to upload a polygon and rectangle at the same time + pred = rectangle_inference.copy() + del pred['name'] + _validate_ndjson([pred], configured_project) + + def test_missing_feature_schema(configured_project, rectangle_inference): #Trying to upload a polygon and rectangle at the same time pred = rectangle_inference.copy() del pred['schemaId'] + del pred['name'] with pytest.raises(MALValidationError): _validate_ndjson([pred], configured_project) From e83b12c86a1923cbba227d45bf4b68da49f1bef8 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 13 Jul 2022 19:48:21 +0300 Subject: [PATCH 15/20] serialization and deserialization tests --- labelbox/data/annotation_types/collection.py | 3 +- labelbox/data/annotation_types/label.py | 3 +- labelbox/data/serialization/ndjson/base.py | 20 +- .../serialization/ndjson/classification.py | 27 +-- labelbox/schema/bulk_import_request.py | 4 +- .../classification_import_name_only.json | 20 ++ .../assets/ndjson/image_import_name_only.json | 215 ++++++++++++++++++ .../ndjson/nested_import_name_only.json | 48 ++++ .../ndjson/test_classification.py | 12 + tests/data/serialization/ndjson/test_image.py | 10 + .../data/serialization/ndjson/test_nested.py | 7 + 11 files changed, 335 insertions(+), 34 deletions(-) create mode 100644 tests/data/assets/ndjson/classification_import_name_only.json create mode 100644 tests/data/assets/ndjson/image_import_name_only.json create mode 100644 tests/data/assets/ndjson/nested_import_name_only.json diff --git a/labelbox/data/annotation_types/collection.py b/labelbox/data/annotation_types/collection.py index 8272efc0d..89fe70a33 100644 --- a/labelbox/data/annotation_types/collection.py +++ b/labelbox/data/annotation_types/collection.py @@ -40,8 +40,7 @@ def assign_feature_schema_ids( Returns: LabelList. useful for chaining these modifying functions - Warning: assign_feature_schema_ids is now obsolete, you can - now use names directly without having to lookup schema_ids. + Note: You can now import annotations using names directly without having to lookup schema_ids """ for label in self._data: label.assign_feature_schema_ids(ontology_builder) diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index ff147aa8c..6699ebfb0 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -137,8 +137,7 @@ def assign_feature_schema_ids( Returns: Label. useful for chaining these modifying functions - Warning: assign_feature_schema_ids is now obsolete, you can - now use names directly without having to lookup schema_ids. + Warning: You can now import annotations using names directly without having to lookup schema_ids """ tool_lookup, classification_lookup = get_feature_schema_lookup( ontology_builder) diff --git a/labelbox/data/serialization/ndjson/base.py b/labelbox/data/serialization/ndjson/base.py index 410ae1340..520446bcd 100644 --- a/labelbox/data/serialization/ndjson/base.py +++ b/labelbox/data/serialization/ndjson/base.py @@ -1,6 +1,6 @@ from typing import Optional from uuid import uuid4 -from pydantic import BaseModel, validator, Field +from pydantic import BaseModel, root_validator, validator, Field from labelbox.utils import camel_case from ...annotation_types.types import Cuid @@ -33,20 +33,16 @@ class Config: class NDAnnotation(NDJsonBase): - schema_id: Optional[Cuid] = None name: Optional[str] = None + schema_id: Optional[Cuid] = None - @validator('name', pre=True, always=True) - def validate_name(cls, v, values): - if v is None and 'schema_id' not in values: - raise ValueError( - "Name and schema_id are not set. Either set name or schema_id.") - - @validator('schema_id', pre=True, always=True) - def validate_id(cls, v, values): - if v is None and 'name' not in values: + @root_validator() + def must_set_one(cls, values): + if ('schema_id' not in values or + values['schema_id'] is None) and ('name' not in values or + values['name'] is None): raise ValueError("Schema id or name are not set. Set either one.") - return v + return values def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 17beefbdb..498f0b297 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Union, Optional -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, root_validator from labelbox.utils import camel_case from ...annotation_types.annotation import ClassificationAnnotation, VideoClassificationAnnotation @@ -14,19 +14,13 @@ class NDFeature(BaseModel): schema_id: Optional[Cuid] = None name: Optional[str] = None - @validator('name', pre=True, always=True) - def validate_name(cls, v, values): - if v is None and 'schema_id' not in values: - raise ValueError( - "Name and schema_id are not set. Either set name or schema_id.") - - @validator('schema_id', pre=True, always=True) - def validate_id(cls, v, values): - if v is None and 'name' not in values: - raise ValueError( - "Schema ids or names are not set. Either set name or schema_id.`." - ) - return v + @root_validator() + def must_set_one(cls, values): + if ('schema_id' not in values or + values['schema_id'] is None) and ('name' not in values or + values['name'] is None): + raise ValueError("Schema id or name are not set. Set either one.") + return values def dict(self, *args, **kwargs): res = super().dict(*args, **kwargs) @@ -84,9 +78,10 @@ def to_common(self) -> Checklist: def from_common(cls, checklist: Checklist, name: str, feature_schema_id: Cuid) -> "NDChecklistSubclass": return cls(answer=[ - NDFeature(name=name, schema_id=answer.feature_schema_id) + NDFeature(name=answer.name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], + name=name, schema_id=feature_schema_id) def dict(self, *args, **kwargs): @@ -138,7 +133,7 @@ def from_common( extra: Dict[str, Any], data: Union[VideoData, TextData, ImageData]) -> "NDChecklist": return cls(answer=[ - NDFeature(name=name, schema_id=answer.feature_schema_id) + NDFeature(name=answer.name, schema_id=answer.feature_schema_id) for answer in checklist.answer ], data_row={'id': data.uid}, diff --git a/labelbox/schema/bulk_import_request.py b/labelbox/schema/bulk_import_request.py index 5a12c2a85..d340a4610 100644 --- a/labelbox/schema/bulk_import_request.py +++ b/labelbox/schema/bulk_import_request.py @@ -627,7 +627,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, if self.name: if self.name not in valid_feature_schemas_by_name: raise ValueError( - f"name {self.name} is not valid for the provided project's ontology." + f"Name {self.name} is not valid for the provided project's ontology." ) if self.ontology_type != valid_feature_schemas_by_name[ @@ -639,7 +639,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, if self.schemaId: if self.schemaId not in valid_feature_schemas_by_id: raise ValueError( - f"schema id {self.schemaId} is not valid for the provided project's ontology." + f"Schema id {self.schemaId} is not valid for the provided project's ontology." ) if self.ontology_type != valid_feature_schemas_by_id[ diff --git a/tests/data/assets/ndjson/classification_import_name_only.json b/tests/data/assets/ndjson/classification_import_name_only.json new file mode 100644 index 000000000..e7590a6e7 --- /dev/null +++ b/tests/data/assets/ndjson/classification_import_name_only.json @@ -0,0 +1,20 @@ +[ + { + "answer": { "schemaId": "ckrb1sfl8099g0y91cxbd5ftb" }, + "name": "classification a", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "schemaId": "ckrb1sfl8099e0y919v260awv" }], + "name": "classification b", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "answer": "a value", + "name": "classification b", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } +] diff --git a/tests/data/assets/ndjson/image_import_name_only.json b/tests/data/assets/ndjson/image_import_name_only.json new file mode 100644 index 000000000..ada0e3bea --- /dev/null +++ b/tests/data/assets/ndjson/image_import_name_only.json @@ -0,0 +1,215 @@ +[ + { + "uuid": "b862c586-8614-483c-b5e6-82810f70cac0", + "name": "box a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "bbox": { "top": 1352, "left": 2275, "height": 350, "width": 139 } + }, + { + "uuid": "751fc725-f7b6-48ed-89b0-dd7d94d08af6", + "name": "mask a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "mask": { + "instanceURI": "https://storage.labelbox.com/ckqcx1czn06830y61gh9v02cs%2F3e729327-f038-f66c-186e-45e921ef9717-1?Expires=1626806874672&KeyName=labelbox-assets-key-3&Signature=YsUOGKrsqmAZ68vT9BlPJOaRyLY", + "colorRGB": [255, 0, 0] + } + }, + { + "uuid": "43d719ac-5d7f-4aea-be00-2ebfca0900fd", + "name": "polygon a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "polygon": [ + { "x": 1118, "y": 935 }, + { "x": 1117, "y": 935 }, + { "x": 1116, "y": 935 }, + { "x": 1115, "y": 935 }, + { "x": 1114, "y": 935 }, + { "x": 1113, "y": 935 }, + { "x": 1112, "y": 935 }, + { "x": 1111, "y": 935 }, + { "x": 1110, "y": 935 }, + { "x": 1109, "y": 935 }, + { "x": 1108, "y": 935 }, + { "x": 1108, "y": 934 }, + { "x": 1107, "y": 934 }, + { "x": 1106, "y": 934 }, + { "x": 1105, "y": 934 }, + { "x": 1105, "y": 933 }, + { "x": 1104, "y": 933 }, + { "x": 1103, "y": 933 }, + { "x": 1103, "y": 932 }, + { "x": 1102, "y": 932 }, + { "x": 1101, "y": 932 }, + { "x": 1100, "y": 932 }, + { "x": 1099, "y": 932 }, + { "x": 1098, "y": 932 }, + { "x": 1097, "y": 932 }, + { "x": 1097, "y": 931 }, + { "x": 1096, "y": 931 }, + { "x": 1095, "y": 931 }, + { "x": 1094, "y": 931 }, + { "x": 1093, "y": 931 }, + { "x": 1092, "y": 931 }, + { "x": 1091, "y": 931 }, + { "x": 1090, "y": 931 }, + { "x": 1090, "y": 930 }, + { "x": 1089, "y": 930 }, + { "x": 1088, "y": 930 }, + { "x": 1087, "y": 930 }, + { "x": 1087, "y": 929 }, + { "x": 1086, "y": 929 }, + { "x": 1085, "y": 929 }, + { "x": 1084, "y": 929 }, + { "x": 1084, "y": 928 }, + { "x": 1083, "y": 928 }, + { "x": 1083, "y": 927 }, + { "x": 1082, "y": 927 }, + { "x": 1081, "y": 927 }, + { "x": 1081, "y": 926 }, + { "x": 1080, "y": 926 }, + { "x": 1080, "y": 925 }, + { "x": 1079, "y": 925 }, + { "x": 1078, "y": 925 }, + { "x": 1078, "y": 924 }, + { "x": 1077, "y": 924 }, + { "x": 1076, "y": 924 }, + { "x": 1076, "y": 923 }, + { "x": 1075, "y": 923 }, + { "x": 1074, "y": 923 }, + { "x": 1073, "y": 923 }, + { "x": 1073, "y": 922 }, + { "x": 1072, "y": 922 }, + { "x": 1071, "y": 922 }, + { "x": 1070, "y": 922 }, + { "x": 1070, "y": 921 }, + { "x": 1069, "y": 921 }, + { "x": 1068, "y": 921 }, + { "x": 1067, "y": 921 }, + { "x": 1066, "y": 921 }, + { "x": 1065, "y": 921 }, + { "x": 1064, "y": 921 }, + { "x": 1063, "y": 921 }, + { "x": 1062, "y": 921 }, + { "x": 1061, "y": 921 }, + { "x": 1060, "y": 921 }, + { "x": 1059, "y": 921 }, + { "x": 1058, "y": 921 }, + { "x": 1058, "y": 920 }, + { "x": 1057, "y": 920 }, + { "x": 1057, "y": 919 }, + { "x": 1056, "y": 919 }, + { "x": 1057, "y": 918 }, + { "x": 1057, "y": 918 }, + { "x": 1057, "y": 917 }, + { "x": 1058, "y": 916 }, + { "x": 1058, "y": 916 }, + { "x": 1059, "y": 915 }, + { "x": 1059, "y": 915 }, + { "x": 1060, "y": 914 }, + { "x": 1060, "y": 914 }, + { "x": 1061, "y": 913 }, + { "x": 1061, "y": 913 }, + { "x": 1062, "y": 912 }, + { "x": 1063, "y": 912 }, + { "x": 1063, "y": 912 }, + { "x": 1064, "y": 911 }, + { "x": 1064, "y": 911 }, + { "x": 1065, "y": 910 }, + { "x": 1066, "y": 910 }, + { "x": 1066, "y": 910 }, + { "x": 1067, "y": 909 }, + { "x": 1068, "y": 909 }, + { "x": 1068, "y": 909 }, + { "x": 1069, "y": 908 }, + { "x": 1070, "y": 908 }, + { "x": 1071, "y": 908 }, + { "x": 1072, "y": 908 }, + { "x": 1072, "y": 908 }, + { "x": 1073, "y": 907 }, + { "x": 1074, "y": 907 }, + { "x": 1075, "y": 907 }, + { "x": 1076, "y": 907 }, + { "x": 1077, "y": 907 }, + { "x": 1078, "y": 907 }, + { "x": 1079, "y": 907 }, + { "x": 1080, "y": 907 }, + { "x": 1081, "y": 907 }, + { "x": 1082, "y": 907 }, + { "x": 1083, "y": 907 }, + { "x": 1084, "y": 907 }, + { "x": 1085, "y": 907 }, + { "x": 1086, "y": 907 }, + { "x": 1087, "y": 907 }, + { "x": 1088, "y": 907 }, + { "x": 1089, "y": 907 }, + { "x": 1090, "y": 907 }, + { "x": 1091, "y": 907 }, + { "x": 1091, "y": 908 }, + { "x": 1092, "y": 908 }, + { "x": 1093, "y": 908 }, + { "x": 1094, "y": 908 }, + { "x": 1095, "y": 908 }, + { "x": 1095, "y": 909 }, + { "x": 1096, "y": 909 }, + { "x": 1097, "y": 909 }, + { "x": 1097, "y": 910 }, + { "x": 1098, "y": 910 }, + { "x": 1099, "y": 910 }, + { "x": 1099, "y": 911 }, + { "x": 1100, "y": 911 }, + { "x": 1101, "y": 911 }, + { "x": 1101, "y": 912 }, + { "x": 1102, "y": 912 }, + { "x": 1103, "y": 912 }, + { "x": 1103, "y": 913 }, + { "x": 1104, "y": 913 }, + { "x": 1104, "y": 914 }, + { "x": 1105, "y": 914 }, + { "x": 1105, "y": 915 }, + { "x": 1106, "y": 915 }, + { "x": 1107, "y": 915 }, + { "x": 1107, "y": 916 }, + { "x": 1108, "y": 916 }, + { "x": 1108, "y": 917 }, + { "x": 1109, "y": 917 }, + { "x": 1109, "y": 918 }, + { "x": 1110, "y": 918 }, + { "x": 1110, "y": 919 }, + { "x": 1111, "y": 919 }, + { "x": 1111, "y": 920 }, + { "x": 1112, "y": 920 }, + { "x": 1112, "y": 921 }, + { "x": 1113, "y": 921 }, + { "x": 1113, "y": 922 }, + { "x": 1114, "y": 922 }, + { "x": 1114, "y": 923 }, + { "x": 1115, "y": 923 }, + { "x": 1115, "y": 924 }, + { "x": 1115, "y": 925 }, + { "x": 1116, "y": 925 }, + { "x": 1116, "y": 926 }, + { "x": 1117, "y": 926 }, + { "x": 1117, "y": 927 }, + { "x": 1117, "y": 928 }, + { "x": 1118, "y": 928 }, + { "x": 1118, "y": 929 }, + { "x": 1119, "y": 929 }, + { "x": 1119, "y": 930 }, + { "x": 1120, "y": 930 }, + { "x": 1120, "y": 931 }, + { "x": 1120, "y": 932 }, + { "x": 1120, "y": 932 }, + { "x": 1119, "y": 933 }, + { "x": 1119, "y": 934 }, + { "x": 1119, "y": 934 }, + { "x": 1118, "y": 935 }, + { "x": 1118, "y": 935 } + ] + }, + { + "uuid": "b98f3a45-3328-41a0-9077-373a8177ebf2", + "name": "point a", + "dataRow": { "id": "ckrazctum0z8a0ybc0b0o0g0v" }, + "point": { "x": 2122, "y": 1457 } + } +] diff --git a/tests/data/assets/ndjson/nested_import_name_only.json b/tests/data/assets/ndjson/nested_import_name_only.json new file mode 100644 index 000000000..9ca899d95 --- /dev/null +++ b/tests/data/assets/ndjson/nested_import_name_only.json @@ -0,0 +1,48 @@ +[ + { + "bbox": { "height": 350, "left": 2275, "top": 1352, "width": 139 }, + "classifications": [ + { + "answer": { "name": "first answer" }, + "name": "classification a" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box a", + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { + "answer": { "name": "second answer" }, + "name": "classification b" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box b", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { + "answer": [{ "name": "third answer" }], + "name": "classification c" + } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box c", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "bbox": { "height": 428, "left": 2089, "top": 1251, "width": 158 }, + "classifications": [ + { "answer": "a string", "name": "a string" } + ], + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "name": "box c", + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } + ] + \ No newline at end of file diff --git a/tests/data/serialization/ndjson/test_classification.py b/tests/data/serialization/ndjson/test_classification.py index b42825e33..cf6ab326d 100644 --- a/tests/data/serialization/ndjson/test_classification.py +++ b/tests/data/serialization/ndjson/test_classification.py @@ -1,6 +1,9 @@ +import inspect import json +from labelbox.data.serialization.ndjson.classification import NDRadio from labelbox.data.serialization.ndjson.converter import NDJsonConverter +from labelbox.data.serialization.ndjson.objects import NDLine def test_classification(): @@ -10,3 +13,12 @@ def test_classification(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + + +def test_classification_with_name(): + with open('tests/data/assets/ndjson/classification_import_name_only.json', + 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data diff --git a/tests/data/serialization/ndjson/test_image.py b/tests/data/serialization/ndjson/test_image.py index 4b4825fd5..993f30cb4 100644 --- a/tests/data/serialization/ndjson/test_image.py +++ b/tests/data/serialization/ndjson/test_image.py @@ -29,6 +29,16 @@ def test_image(): r.pop('classifications', None) assert [round_dict(x) for x in res] == [round_dict(x) for x in data] +def test_image_with_name_only(): + with open('tests/data/assets/ndjson/image_import_name_only.json', 'r') as file: + data = json.load(file) + + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + for r in res: + r.pop('classifications', None) + assert [round_dict(x) for x in res] == [round_dict(x) for x in data] + def test_mask(): data = [{ diff --git a/tests/data/serialization/ndjson/test_nested.py b/tests/data/serialization/ndjson/test_nested.py index 64fd199ff..7c05271f7 100644 --- a/tests/data/serialization/ndjson/test_nested.py +++ b/tests/data/serialization/ndjson/test_nested.py @@ -9,3 +9,10 @@ def test_nested(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + +def test_nested_name_only(): + with open('tests/data/assets/ndjson/nested_import_name_only.json', 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data From 611b80da544208edbcfb1be4dca85e1ca0ef3c59 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 13 Jul 2022 19:53:45 +0300 Subject: [PATCH 16/20] formatting --- tests/data/serialization/ndjson/test_classification.py | 1 - tests/data/serialization/ndjson/test_image.py | 4 +++- tests/data/serialization/ndjson/test_nested.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/data/serialization/ndjson/test_classification.py b/tests/data/serialization/ndjson/test_classification.py index cf6ab326d..c897717d4 100644 --- a/tests/data/serialization/ndjson/test_classification.py +++ b/tests/data/serialization/ndjson/test_classification.py @@ -1,4 +1,3 @@ -import inspect import json from labelbox.data.serialization.ndjson.classification import NDRadio diff --git a/tests/data/serialization/ndjson/test_image.py b/tests/data/serialization/ndjson/test_image.py index 993f30cb4..242604ad9 100644 --- a/tests/data/serialization/ndjson/test_image.py +++ b/tests/data/serialization/ndjson/test_image.py @@ -29,8 +29,10 @@ def test_image(): r.pop('classifications', None) assert [round_dict(x) for x in res] == [round_dict(x) for x in data] + def test_image_with_name_only(): - with open('tests/data/assets/ndjson/image_import_name_only.json', 'r') as file: + with open('tests/data/assets/ndjson/image_import_name_only.json', + 'r') as file: data = json.load(file) res = NDJsonConverter.deserialize(data).as_list() diff --git a/tests/data/serialization/ndjson/test_nested.py b/tests/data/serialization/ndjson/test_nested.py index 7c05271f7..54b2d32ee 100644 --- a/tests/data/serialization/ndjson/test_nested.py +++ b/tests/data/serialization/ndjson/test_nested.py @@ -10,8 +10,10 @@ def test_nested(): res = list(NDJsonConverter.serialize(res)) assert res == data + def test_nested_name_only(): - with open('tests/data/assets/ndjson/nested_import_name_only.json', 'r') as file: + with open('tests/data/assets/ndjson/nested_import_name_only.json', + 'r') as file: data = json.load(file) res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) From 4ed066306983b991677348bc91e415e2890bf8fb Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 13 Jul 2022 20:11:09 +0300 Subject: [PATCH 17/20] add name to video bbox --- tests/data/serialization/ndjson/test_export_video_objects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/data/serialization/ndjson/test_export_video_objects.py b/tests/data/serialization/ndjson/test_export_video_objects.py index a10fef1be..84434d786 100644 --- a/tests/data/serialization/ndjson/test_export_video_objects.py +++ b/tests/data/serialization/ndjson/test_export_video_objects.py @@ -526,6 +526,8 @@ def video_serialized_bbox_label(): }, 'schemaId': 'ckz38ofop0mci0z9i9w3aa9o4', + 'name': + 'bbox toy', 'classifications': [], 'segments': [{ 'keyframes': [{ From b2048e8bec89c1ea2df79789e86ee3779b53f5dd Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 13 Jul 2022 22:36:32 +0300 Subject: [PATCH 18/20] fix field name and answers --- .../serialization/ndjson/classification.py | 2 +- labelbox/schema/bulk_import_request.py | 23 +++++-------------- .../classification_import_name_only.json | 6 ++--- .../integration/annotation_import/conftest.py | 2 +- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/labelbox/data/serialization/ndjson/classification.py b/labelbox/data/serialization/ndjson/classification.py index 498f0b297..dd4027238 100644 --- a/labelbox/data/serialization/ndjson/classification.py +++ b/labelbox/data/serialization/ndjson/classification.py @@ -11,8 +11,8 @@ class NDFeature(BaseModel): - schema_id: Optional[Cuid] = None name: Optional[str] = None + schema_id: Optional[Cuid] = None @root_validator() def must_set_one(cls, values): diff --git a/labelbox/schema/bulk_import_request.py b/labelbox/schema/bulk_import_request.py index d340a4610..b98fd3c0d 100644 --- a/labelbox/schema/bulk_import_request.py +++ b/labelbox/schema/bulk_import_request.py @@ -675,20 +675,9 @@ class NDText(NDBase): #No feature schema to check -class NDAnswer(BaseModel): - schemaId: Optional[Cuid] = None - value: Optional[str] = None - - @root_validator - def must_set_one(cls, values): - if values['schemaId'] is None and values['value'] is None: - raise ValueError("Must set either schemaId or value for answers") - return values - - class NDChecklist(VideoSupported, NDBase): ontology_type: Literal["checklist"] = "checklist" - answers: List[NDAnswer] = pydantic.Field(determinant=True) + answers: List[NDFeatureSchema] = pydantic.Field(determinant=True) @validator('answers', pre=True) def validate_answers(cls, value, field): @@ -704,7 +693,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, self).validate_feature_schemas(valid_feature_schemas_by_id, valid_feature_schemas_by_name) #Test the feature schemas provided to the answer field - if len(set([answer.value or answer.schemaId for answer in self.answers + if len(set([answer.name or answer.schemaId for answer in self.answers ])) != len(self.answers): raise ValueError( f"Duplicated featureSchema found for checklist {self.uuid}") @@ -713,7 +702,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, self. name]['options'] if self.name else valid_feature_schemas_by_id[ self.schemaId]['options'] - if answer.value not in options and answer.schemaId not in options: + if answer.name not in options and answer.schemaId not in options: raise ValueError( f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {answer}" ) @@ -721,7 +710,7 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, class NDRadio(VideoSupported, NDBase): ontology_type: Literal["radio"] = "radio" - answer: NDAnswer = pydantic.Field(determinant=True) + answer: NDFeatureSchema = pydantic.Field(determinant=True) def validate_feature_schemas(self, valid_feature_schemas_by_id, valid_feature_schemas_by_name): @@ -731,9 +720,9 @@ def validate_feature_schemas(self, valid_feature_schemas_by_id, options = valid_feature_schemas_by_name[ self.name]['options'] if self.name else valid_feature_schemas_by_id[ self.schemaId]['options'] - if self.answer.value not in options and self.answer.schemaId not in options: + if self.answer.name not in options and self.answer.schemaId not in options: raise ValueError( - f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.value or self.answer.schemaId}" + f"Feature schema provided to {self.ontology_type} invalid. Expected on of {options}. Found {self.answer.name or self.answer.schemaId}" ) diff --git a/tests/data/assets/ndjson/classification_import_name_only.json b/tests/data/assets/ndjson/classification_import_name_only.json index e7590a6e7..0261450f2 100644 --- a/tests/data/assets/ndjson/classification_import_name_only.json +++ b/tests/data/assets/ndjson/classification_import_name_only.json @@ -1,19 +1,19 @@ [ { - "answer": { "schemaId": "ckrb1sfl8099g0y91cxbd5ftb" }, + "answer": { "name": "choice 1" }, "name": "classification a", "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" }, { - "answer": [{ "schemaId": "ckrb1sfl8099e0y919v260awv" }], + "answer": [{ "name": "choice 2" }], "name": "classification b", "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" }, { "answer": "a value", - "name": "classification b", + "name": "classification c", "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" } diff --git a/tests/integration/annotation_import/conftest.py b/tests/integration/annotation_import/conftest.py index b50c04716..c00df1ce3 100644 --- a/tests/integration/annotation_import/conftest.py +++ b/tests/integration/annotation_import/conftest.py @@ -185,7 +185,7 @@ def rectangle_inference(prediction_id_mapping): "schemaId": rectangle['tool']['classifications'][0]['options'][0] ['featureSchemaId'], - "value": + "name": rectangle['tool']['classifications'][0]['options'][0] ['value'] } From c0773277f25507e5e3cd91299333b259f7a14128 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Wed, 13 Jul 2022 22:54:01 +0300 Subject: [PATCH 19/20] fix semantic conflict and add more tests --- labelbox/data/serialization/ndjson/objects.py | 8 +- tests/data/assets/ndjson/text_import.json | 15 ++- .../assets/ndjson/text_import_name_only.json | 15 +++ .../assets/ndjson/video_import_name_only.json | 94 +++++++++++++++++++ tests/data/serialization/ndjson/test_text.py | 9 ++ tests/data/serialization/ndjson/test_video.py | 10 ++ 6 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 tests/data/assets/ndjson/text_import_name_only.json create mode 100644 tests/data/assets/ndjson/video_import_name_only.json diff --git a/labelbox/data/serialization/ndjson/objects.py b/labelbox/data/serialization/ndjson/objects.py index 8f8703e1e..f2200e8f5 100644 --- a/labelbox/data/serialization/ndjson/objects.py +++ b/labelbox/data/serialization/ndjson/objects.py @@ -65,9 +65,11 @@ def from_common(cls, point: Point, class NDFramePoint(VideoSupported): point: _Point - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation(frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Point(x=self.point.x, y=self.point.y)) @@ -102,10 +104,12 @@ def from_common(cls, line: Line, class NDFrameLine(VideoSupported): line: List[_Point] - def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation: + def to_common(self, name: str, + feature_schema_id: Cuid) -> VideoObjectAnnotation: return VideoObjectAnnotation( frame=self.frame, keyframe=True, + name=name, feature_schema_id=feature_schema_id, value=Line(points=[Point(x=pt.x, y=pt.y) for pt in self.line])) diff --git a/tests/data/assets/ndjson/text_import.json b/tests/data/assets/ndjson/text_import.json index 6aa682802..d17c97ba4 100644 --- a/tests/data/assets/ndjson/text_import.json +++ b/tests/data/assets/ndjson/text_import.json @@ -1 +1,14 @@ -[{"answer": {"schemaId": "ckrb1sfl8099g0y91cxbd5ftb"}, "schemaId": "ckrb1sfjx099a0y914hl319ie", "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673"}, {"answer": [{"schemaId": "ckrb1sfl8099e0y919v260awv"}], "schemaId": "ckrb1sfkn099c0y910wbo0p1a", "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, "uuid": "d009925d-91a3-4f67-abd9-753453f5a584"}] \ No newline at end of file +[ + { + "answer": { "schemaId": "ckrb1sfl8099g0y91cxbd5ftb" }, + "schemaId": "ckrb1sfjx099a0y914hl319ie", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "schemaId": "ckrb1sfl8099e0y919v260awv" }], + "schemaId": "ckrb1sfkn099c0y910wbo0p1a", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } +] diff --git a/tests/data/assets/ndjson/text_import_name_only.json b/tests/data/assets/ndjson/text_import_name_only.json new file mode 100644 index 000000000..cf1477988 --- /dev/null +++ b/tests/data/assets/ndjson/text_import_name_only.json @@ -0,0 +1,15 @@ +[ + { + "answer": { "name": "answer a" }, + "name": "question 1", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673" + }, + { + "answer": [{ "name": "answer b" }], + "name": "question 2", + "dataRow": { "id": "ckrb1sf1i1g7i0ybcdc6oc8ct" }, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + } + ] + \ No newline at end of file diff --git a/tests/data/assets/ndjson/video_import_name_only.json b/tests/data/assets/ndjson/video_import_name_only.json new file mode 100644 index 000000000..acdc42e6e --- /dev/null +++ b/tests/data/assets/ndjson/video_import_name_only.json @@ -0,0 +1,94 @@ +[ + { + "answer": {"name": "answer 1"}, + "name": "question 1", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "f6879f59-d2b5-49c2-aceb-d9e8dc478673", + "frames": [{"start": 30, "end": 35}, {"start": 50, "end": 51}] + }, + { + "answer": [{"name": "answer 2"}], + "name": "question 2", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584", + "frames": [{"start": 0, "end": 5}] + }, + { + "answer": "a value", + "name": "question 3", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "d009925d-91a3-4f67-abd9-753453f5a584" + }, + { + "classifications": [], + "name": "segment 1", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "6f7c835a-0139-4896-b73f-66a6baa89e94", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "line": [{"x": 10.0, "y": 10.0}, {"x": 100.0, "y": 100.0}, {"x": 50.0, "y": 30.0}] + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "line": [{"x": 100.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}] + } + ] + } + ] + }, + { + "classifications": [], + "name": "segment 2", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "f963be22-227b-4efe-9be4-2738ed822216", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "point": {"x": 10.0, "y": 10.0} + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "point": {"x": 50.0, "y": 50.0} + } + ] + } + ] + }, + { + "classifications": [], + "name": "segment 3", + "dataRow": {"id": "ckrb1sf1i1g7i0ybcdc6oc8ct"}, + "uuid": "13b2ee0e-2355-4336-8b83-d74d09e3b1e7", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "bbox": {"top": 10.0, "left": 5.0, "height": 100.0, "width": 150.0} + } + ] + }, + { + "keyframes": [ + { + "frame": 5, + "bbox": {"top": 300.0, "left": 200.0, "height": 400.0, "width": 150.0} + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/data/serialization/ndjson/test_text.py b/tests/data/serialization/ndjson/test_text.py index 1c1b69fe2..30e0f6e46 100644 --- a/tests/data/serialization/ndjson/test_text.py +++ b/tests/data/serialization/ndjson/test_text.py @@ -9,3 +9,12 @@ def test_text(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == data + + +def test_text_name_only(): + with open('tests/data/assets/ndjson/text_import_name_only.json', + 'r') as file: + data = json.load(file) + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == data diff --git a/tests/data/serialization/ndjson/test_video.py b/tests/data/serialization/ndjson/test_video.py index 01eda7175..baa029f34 100644 --- a/tests/data/serialization/ndjson/test_video.py +++ b/tests/data/serialization/ndjson/test_video.py @@ -10,3 +10,13 @@ def test_video(): res = NDJsonConverter.deserialize(data).as_list() res = list(NDJsonConverter.serialize(res)) assert res == [data[2], data[0], data[1], data[3], data[4], data[5]] + + +def test_video_name_only(): + with open('tests/data/assets/ndjson/video_import_name_only.json', + 'r') as file: + data = json.load(file) + + res = NDJsonConverter.deserialize(data).as_list() + res = list(NDJsonConverter.serialize(res)) + assert res == [data[2], data[0], data[1], data[3], data[4], data[5]] From b08d63aad5c439b8ba78e87b86050a5b1c4386d5 Mon Sep 17 00:00:00 2001 From: Faruk Can Bilir Date: Thu, 14 Jul 2022 00:32:28 +0300 Subject: [PATCH 20/20] update comment, remove unnecessary import --- labelbox/data/annotation_types/label.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/labelbox/data/annotation_types/label.py b/labelbox/data/annotation_types/label.py index 6699ebfb0..4f5539214 100644 --- a/labelbox/data/annotation_types/label.py +++ b/labelbox/data/annotation_types/label.py @@ -1,6 +1,5 @@ from collections import defaultdict from typing import Any, Callable, Dict, List, Union, Optional -import warnings from pydantic import BaseModel, validator @@ -137,7 +136,7 @@ def assign_feature_schema_ids( Returns: Label. useful for chaining these modifying functions - Warning: You can now import annotations using names directly without having to lookup schema_ids + Note: You can now import annotations using names directly without having to lookup schema_ids """ tool_lookup, classification_lookup = get_feature_schema_lookup( ontology_builder)