Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ba3b316
first commit of this branch
jtsodapop Jan 14, 2021
c053bf7
updating the file so that it now properly pulls in Options when looki…
jtsodapop Jan 19, 2021
94f0f78
small edit
jtsodapop Jan 19, 2021
a58f99c
updates to the file to have helper functions that smoothen the writeup
jtsodapop Feb 4, 2021
3ce0fc5
updates to the file to have helper functions that smoothen the writeup
jtsodapop Feb 4, 2021
e0b3d8e
able to now get and set basic ontologies
jtsodapop Feb 10, 2021
deec7f2
able to now get and set basic ontologies
jtsodapop Feb 10, 2021
e2740af
able to now get and set basic ontologies
jtsodapop Feb 10, 2021
5376a1e
able to now get and set basic ontologies
jtsodapop Feb 10, 2021
9cc29ef
added the ability to strip schema ids and feature schema ids for when…
jtsodapop Feb 11, 2021
021d11b
added NER tool
jtsodapop Feb 12, 2021
c910fad
added NER tool
jtsodapop Feb 12, 2021
aed5a98
made minor adjustments
jtsodapop Feb 13, 2021
e44d672
made minor adjustments
jtsodapop Feb 13, 2021
0ecb479
cleanup
jtsodapop Feb 13, 2021
7cda334
updates to make the code more consistent and cleaner
jtsodapop Feb 16, 2021
f74d027
added some commentary on potential pitfalls
jtsodapop Feb 16, 2021
76dfc14
some cleanup on length
jtsodapop Feb 17, 2021
d8d16d4
shortening of lines and cleaning up code
jtsodapop Feb 17, 2021
c40ebb7
shortening of lines and cleaning up code
jtsodapop Feb 17, 2021
1152dd3
small nit changes
jtsodapop Feb 18, 2021
ef191c8
nit changes
jtsodapop Feb 23, 2021
479cc74
changes
jtsodapop Feb 23, 2021
f575abb
updated tests
jtsodapop Feb 26, 2021
7cd448a
updated tests
jtsodapop Feb 26, 2021
312ef7f
update to testing
jtsodapop Mar 1, 2021
5c27405
update to testing
jtsodapop Mar 1, 2021
07388cb
update to tests
jtsodapop Mar 2, 2021
5e7180e
add ontology.from_dict and updated tests
jtsodapop Mar 4, 2021
c570276
making pep8 format
jtsodapop Mar 10, 2021
0a7c603
updating ontology classes to ontology file
jtsodapop Mar 10, 2021
6509054
added docstrings and method to create colors if empty
jtsodapop Mar 11, 2021
6f523c2
added docstrings and method to create colors if empty
jtsodapop Mar 11, 2021
32aeebf
added docstrings and method to create colors if empty
jtsodapop Mar 11, 2021
fde2538
resolving conflicts
jtsodapop Mar 11, 2021
e0c7320
update
jtsodapop Mar 11, 2021
db64b0f
update
jtsodapop Mar 11, 2021
14f0072
update
jtsodapop Mar 11, 2021
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
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ FROM python:3.7

RUN pip install pytest pytest-cases


WORKDIR /usr/src/labelbox
COPY requirements.txt /usr/src/labelbox
RUN pip install -r requirements.txt
Expand Down
4 changes: 4 additions & 0 deletions labelbox/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ class UuidError(LabelboxError):
pass


class InconsistentOntologyException(Exception):
pass


class MALValidationError(LabelboxError):
"""Raised when user input is invalid for MAL imports."""
...
322 changes: 276 additions & 46 deletions labelbox/schema/ontology.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,234 @@
import abc
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum, auto
import colorsys

from typing import Any, Callable, Dict, List, Optional, Union

from labelbox.schema.project import Project
from labelbox.orm import query
from labelbox.orm.db_object import DbObject, Updateable, BulkDeletable
from labelbox.orm.model import Entity, Field, Relationship
from labelbox.utils import snake_case, camel_case
from labelbox.exceptions import InconsistentOntologyException


@dataclass
class OntologyEntity:
required: bool
name: str
class Option:
"""
An option is a possible answer within a Classification object in
a Project's ontology.

To instantiate, only the "value" parameter needs to be passed in.

@dataclass
class Option:
label: str
value: str
Example(s):
option = Option(value = "Option Example")

Attributes:
value: (str)
schema_id: (str)
feature_schema_id: (str)
options: (list)
"""
value: Union[str, int]
schema_id: Optional[str] = None
feature_schema_id: Optional[str] = None
schema_node_id: Optional[str] = None
options: List["Classification"] = field(default_factory=list)

@property
def label(self):
return self.value

@classmethod
def from_json(cls, json_dict):
_dict = convert_keys(json_dict, snake_case)
return cls(**_dict)
def from_dict(cls, dictionary: Dict[str, Any]):
return Option(value=dictionary["value"],
schema_id=dictionary.get("schemaNodeId", []),
feature_schema_id=dictionary.get("featureSchemaId", []),
options=[
Classification.from_dict(o)
for o in dictionary.get("options", [])
])

def asdict(self) -> Dict[str, Any]:
return {
"schemaNodeId": self.schema_id,
"featureSchemaId": self.feature_schema_id,
"label": self.label,
"value": self.value,
"options": [o.asdict() for o in self.options]
}

def add_option(self, option: 'Classification'):
if option.instructions in (o.instructions for o in self.options):
raise InconsistentOntologyException(
f"Duplicate nested classification '{option.instructions}' "
f"for option '{self.label}'")
self.options.append(option)


@dataclass
class Classification(OntologyEntity):
type: str
class Classification:
"""
A classfication to be added to a Project's ontology. The
classification is dependent on the Classification Type.

To instantiate, the "class_type" and "instructions" parameters must
be passed in.

The "options" parameter holds a list of Option objects. This is not
necessary for some Classification types, such as TEXT. To see which
types require options, look at the "_REQUIRES_OPTIONS" class variable.

Example(s):
classification = Classification(
class_type = Classification.Type.TEXT,
instructions = "Classification Example")

classification_two = Classification(
class_type = Classification.Type.RADIO,
instructions = "Second Example")
classification_two.add_option(Option(
value = "Option Example"))

Attributes:
class_type: (Classification.Type)
instructions: (str)
required: (bool)
options: (list)
schema_id: (str)
feature_schema_id: (str)
"""

class Type(Enum):
TEXT = "text"
CHECKLIST = "checklist"
RADIO = "radio"
DROPDOWN = "dropdown"

_REQUIRES_OPTIONS = {Type.CHECKLIST, Type.RADIO, Type.DROPDOWN}

class_type: Type
instructions: str
options: List[Option]
required: bool = False
options: List[Option] = field(default_factory=list)
schema_id: Optional[str] = None
feature_schema_id: Optional[str] = None
schema_node_id: Optional[str] = None

@property
def name(self):
return self.instructions

@classmethod
def from_json(cls, json_dict):
_dict = convert_keys(json_dict, snake_case)
_dict['options'] = [
Option.from_json(option) for option in _dict['options']
]
return cls(**_dict)
def from_dict(cls, dictionary: Dict[str, Any]):
return Classification(
class_type=Classification.Type(dictionary["type"]),
instructions=dictionary["instructions"],
required=dictionary["required"],
options=[Option.from_dict(o) for o in dictionary["options"]],
schema_id=dictionary.get("schemaNodeId", []),
feature_schema_id=dictionary.get("featureSchemaId", []))

def asdict(self) -> Dict[str, Any]:
if self.class_type in Classification._REQUIRES_OPTIONS \
and len(self.options) < 1:
raise InconsistentOntologyException(
f"Classification '{self.instructions}' requires options.")
return {
"type": self.class_type.value,
"instructions": self.instructions,
"name": self.name,
"required": self.required,
"options": [o.asdict() for o in self.options],
"schemaNodeId": self.schema_id,
"featureSchemaId": self.feature_schema_id
}

def add_option(self, option: Option):
if option.value in (o.value for o in self.options):
raise InconsistentOntologyException(
f"Duplicate option '{option.value}' "
f"for classification '{self.name}'.")
self.options.append(option)


@dataclass
class Tool(OntologyEntity):
tool: str
color: str
classifications: List[Classification]
class Tool:
"""
A tool to be added to a Project's ontology. The tool is
dependent on the Tool Type.

To instantiate, the "tool" and "name" parameters must
be passed in.

The "classifications" parameter holds a list of Classification objects.
This can be used to add nested classifications to a tool.

Example(s):
tool = Tool(
tool = Tool.Type.LINE,
name = "Tool example")
classification = Classification(
class_type = Classification.Type.TEXT,
instructions = "Classification Example")
tool.add_classification(classification)

Attributes:
tool: (Tool.Type)
name: (str)
required: (bool)
color: (str)
classifications: (list)
schema_id: (str)
feature_schema_id: (str)
"""

class Type(Enum):
POLYGON = "polygon"
SEGMENTATION = "superpixel"
POINT = "point"
BBOX = "rectangle"
LINE = "line"
NER = "named-entity"

tool: Type
name: str
required: bool = False
color: Optional[str] = None
classifications: List[Classification] = field(default_factory=list)
schema_id: Optional[str] = None
feature_schema_id: Optional[str] = None
schema_node_id: Optional[str] = None

@classmethod
def from_json(cls, json_dict):
_dict = convert_keys(json_dict, snake_case)
_dict['classifications'] = [
Classification.from_json(classification)
for classification in _dict['classifications']
]
return cls(**_dict)
def from_dict(cls, dictionary: Dict[str, Any]):
return Tool(name=dictionary['name'],
schema_id=dictionary.get("schemaNodeId", []),
feature_schema_id=dictionary.get("featureSchemaId", []),
required=dictionary["required"],
tool=Tool.Type(dictionary["tool"]),
classifications=[
Classification.from_dict(c)
for c in dictionary["classifications"]
],
color=dictionary["color"])

def asdict(self) -> Dict[str, Any]:
return {
"tool": self.tool.value,
"name": self.name,
"required": self.required,
"color": self.color,
"classifications": [c.asdict() for c in self.classifications],
"schemaNodeId": self.schema_id,
"featureSchemaId": self.feature_schema_id
}

def add_classification(self, classification: Classification):
if classification.instructions in (
c.instructions for c in self.classifications):
raise InconsistentOntologyException(
f"Duplicate nested classification '{classification.instructions}' "
f"for tool '{self.name}'")
self.classifications.append(classification)


class Ontology(DbObject):
Expand Down Expand Up @@ -98,27 +266,89 @@ def tools(self) -> List[Tool]:
"""Get list of tools (AKA objects) in an Ontology."""
if self._tools is None:
self._tools = [
Tool.from_json(tool) for tool in self.normalized['tools']
Tool.from_dict(tool) for tool in self.normalized['tools']
]
return self._tools # type: ignore
return self._tools

def classifications(self) -> List[Classification]:
"""Get list of classifications in an Ontology."""
if self._classifications is None:
self._classifications = [
Classification.from_json(classification)
Classification.from_dict(classification)
for classification in self.normalized['classifications']
]
return self._classifications # type: ignore
return self._classifications


@dataclass
class OntologyBuilder:
"""
A class to help create an ontology for a Project. This should be used
for making Project ontologies from scratch. OntologyBuilder can also
pull from an already existing Project's ontology.

There are no required instantiation arguments.

To create an ontology, use the asdict() method after fully building your
ontology within this class, and inserting it into project.setup() as the
"labeling_frontend_options" parameter.

Example:
builder = OntologyBuilder()
...
frontend = list(client.get_labeling_frontends())[0]
project.setup(frontend, builder.asdict())

def convert_keys(json_dict: Dict[str, Any],
converter: Callable) -> Dict[str, Any]:
if isinstance(json_dict, dict):
attributes:
tools: (list)
classifications: (list)


"""
tools: List[Tool] = field(default_factory=list)
classifications: List[Classification] = field(default_factory=list)

@classmethod
def from_dict(cls, dictionary: Dict[str, Any]):
return OntologyBuilder(
tools=[Tool.from_dict(t) for t in dictionary["tools"]],
classifications=[
Classification.from_dict(c)
for c in dictionary["classifications"]
])

def asdict(self):
self._update_colors()
return {
converter(key): convert_keys(value, converter)
for key, value in json_dict.items()
"tools": [t.asdict() for t in self.tools],
"classifications": [c.asdict() for c in self.classifications]
}
if isinstance(json_dict, list):
return [convert_keys(ele, converter) for ele in json_dict]
return json_dict

def _update_colors(self):
num_tools = len(self.tools)

for index in range(num_tools):
hsv_color = (index * 1 / num_tools, 1, 1)
rgb_color = tuple(
int(255 * x) for x in colorsys.hsv_to_rgb(*hsv_color))
if self.tools[index].color is None:
self.tools[index].color = '#%02x%02x%02x' % rgb_color

@classmethod
def from_project(cls, project: Project):
ontology = project.ontology().normalized
return OntologyBuilder.from_dict(ontology)

def add_tool(self, tool: Tool):
if tool.name in (t.name for t in self.tools):
raise InconsistentOntologyException(
f"Duplicate tool name '{tool.name}'. ")
self.tools.append(tool)

def add_classification(self, classification: Classification):
if classification.instructions in (
c.instructions for c in self.classifications):
raise InconsistentOntologyException(
f"Duplicate classification instructions '{classification.instructions}'. "
)
self.classifications.append(classification)
Loading