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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions labelbox/data/serialization/labelbox_v1/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ def dict(self, *args, **kwargs):

@validator('classifications', pre=True)
def validate_subclasses(cls, value, field):
# Dropdown subclasses create extra unessesary nesting. So we just remove it.
# checklist subclasses create extra unessesary nesting. So we just remove it.
if isinstance(value, list) and len(value):
if isinstance(value[0], list):
return value[0]
subclasses = []
for v in value:
# this is due to Checklists providing extra brackets []. We grab every item
# in the brackets if this is the case
if isinstance(v, list):
for inner_v in v:
subclasses.append(inner_v)
else:
subclasses.append(v)
return subclasses
return value


Expand Down
5 changes: 4 additions & 1 deletion tests/data/serialization/labelbox_v1/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
'tests/data/assets/labelbox_v1/highly_nested_image.json',
'tests/data/assets/labelbox_v1/image_export.json'
])
#TODO: some checklists from the export come in as [checklist ans: []]
# while others are checklist ans: []... when we can figure out why we sometimes
# have extra brackets, we can look into testing nested checklist answers
# and ensuring the export's output matches deserialized/serialized output
def test_image(file_path):
with open(file_path, 'r') as file:
payload = json.load(file)
Expand All @@ -32,7 +36,6 @@ def test_image(file_path):
if isinstance(annotation_b['classifications'][0], list):
annotation_b['classifications'] = annotation_b[
'classifications'][0]

assert annotation_a == annotation_b


Expand Down
54 changes: 53 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from labelbox.pagination import PaginatedCollection
from labelbox.schema.invite import Invite
from labelbox.schema.user import User
from labelbox import OntologyBuilder, Tool
from labelbox import OntologyBuilder, Tool, Option, Classification
from labelbox.schema.annotation_import import LabelImport

IMG_URL = "https://picsum.photos/200/300"
Expand Down Expand Up @@ -333,3 +333,55 @@ def configured_project_with_label(client, rand_gen, image_url):
yield [project, label]
dataset.delete()
project.delete()


@pytest.fixture
def configured_project_with_complex_ontology(client, rand_gen, image_url):
project = client.create_project(name=rand_gen(str))
dataset = client.create_dataset(name=rand_gen(str), projects=project)
data_row = dataset.create_data_row(row_data=image_url)
editor = list(
project.client.get_labeling_frontends(
where=LabelingFrontend.name == "editor"))[0]

ontology = OntologyBuilder()
tools = [
Tool(tool=Tool.Type.BBOX, name="test-bbox-class"),
Tool(tool=Tool.Type.LINE, name="test-line-class"),
Tool(tool=Tool.Type.POINT, name="test-point-class"),
Tool(tool=Tool.Type.POLYGON, name="test-polygon-class"),
Tool(tool=Tool.Type.NER, name="test-ner-class")
]

options = [
Option(value="first option answer"),
Option(value="second option answer"),
Option(value="third option answer")
]

classifications = [
Classification(class_type=Classification.Type.TEXT,
instructions="test-text-class"),
Classification(class_type=Classification.Type.DROPDOWN,
instructions="test-dropdown-class",
options=options),
Classification(class_type=Classification.Type.RADIO,
instructions="test-radio-class",
options=options),
Classification(class_type=Classification.Type.CHECKLIST,
instructions="test-checklist-class",
options=options)
]

for t in tools:
for c in classifications:
t.add_classification(c)
ontology.add_tool(t)
for c in classifications:
ontology.add_classification(c)

project.setup(editor, ontology.asdict())

yield [project, data_row]
dataset.delete()
project.delete()
57 changes: 57 additions & 0 deletions tests/integration/test_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import uuid
from labelbox.data.annotation_types.annotation import ObjectAnnotation

from labelbox.schema.annotation_import import LabelImport


def test_export_annotations_nested_checklist(
client, configured_project_with_complex_ontology):
project, data_row = configured_project_with_complex_ontology
ontology = project.ontology().normalized

tool = ontology["tools"][0]

nested_check = [
subc for subc in tool["classifications"]
if subc["name"] == "test-checklist-class"
][0]

data = [{
"uuid":
str(uuid.uuid4()),
"schemaId":
tool['featureSchemaId'],
"dataRow": {
"id": data_row.uid
},
"bbox": {
"top": 20,
"left": 20,
"height": 50,
"width": 50
},
"classifications": [{
"schemaId":
nested_check["featureSchemaId"],
"answers": [
{
"schemaId": nested_check["options"][0]["featureSchemaId"]
},
{
"schemaId": nested_check["options"][1]["featureSchemaId"]
},
]
}]
}]

task = LabelImport.create_from_objects(client, project.uid,
f'label-import-{uuid.uuid4()}', data)
task.wait_until_done()
labels = project.label_generator().as_list()
object_annotation = [
annot for annot in next(labels).annotations
if isinstance(annot, ObjectAnnotation)
][0]

nested_class_answers = object_annotation.classifications[0].value.answer
assert len(nested_class_answers) == 2