Skip to content

Commit 6509054

Browse files
committed
added docstrings and method to create colors if empty
1 parent 0a7c603 commit 6509054

File tree

1 file changed

+117
-22
lines changed

1 file changed

+117
-22
lines changed

labelbox/schema/ontology.py

Lines changed: 117 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import abc
22
from dataclasses import dataclass, field
33
from enum import Enum, auto
4+
import colorsys
45

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

@@ -14,7 +15,22 @@
1415

1516
@dataclass
1617
class Option:
17-
value: str
18+
"""
19+
An option is a possible answer within a Classification object in
20+
a Project's ontology.
21+
22+
To instantiate, only the "value" parameter needs to be passed in.
23+
24+
Example(s):
25+
option = Option(value = "Option Example")
26+
27+
Attributes:
28+
value: (str)
29+
schema_id: (str)
30+
feature_schema_id: (str)
31+
options: (list)
32+
"""
33+
value: Union[str, int]
1834
schema_id: Optional[str] = None
1935
feature_schema_id: Optional[str] = None
2036
options: List["Classification"] = field(default_factory=list)
@@ -26,8 +42,8 @@ def label(self):
2642
@classmethod
2743
def from_dict(cls, dictionary: Dict[str, Any]):
2844
return Option(value=dictionary["value"],
29-
schema_id=dictionary["schemaNodeId"],
30-
feature_schema_id=dictionary["featureSchemaId"],
45+
schema_id=dictionary.get("schemaNodeId", []),
46+
feature_schema_id=dictionary.get("featureSchemaId", []),
3147
options=[
3248
Classification.from_dict(o)
3349
for o in dictionary.get("options", [])
@@ -52,6 +68,36 @@ def add_option(self, option: 'Classification') -> 'Classification':
5268

5369
@dataclass
5470
class Classification:
71+
"""
72+
A classfication to be added to a Project's ontology. The
73+
classification is dependent on the Classification Type.
74+
75+
To instantiate, the "class_type" and "instructions" parameters must
76+
be passed in.
77+
78+
The "options" parameter holds a list of Option objects. This is not
79+
necessary for some Classification types, such as TEXT. To see which
80+
types require options, look at the "_REQUIRES_OPTIONS" class variable.
81+
82+
Example(s):
83+
classification = Classification(
84+
class_type = Classification.Type.TEXT,
85+
instructions = "Classification Example")
86+
87+
classification_two = Classification(
88+
class_type = Classification.Type.RADIO,
89+
instructions = "Second Example")
90+
classification_two.add_option(Option(
91+
value = "Option Example"))
92+
93+
Attributes:
94+
class_type: (Classification.Type)
95+
instructions: (str)
96+
required: (bool)
97+
options: (list)
98+
schema_id: (str)
99+
feature_schema_id: (str)
100+
"""
55101
class Type(Enum):
56102
TEXT = "text"
57103
CHECKLIST = "checklist"
@@ -78,8 +124,8 @@ def from_dict(cls, dictionary: Dict[str, Any]):
78124
instructions=dictionary["instructions"],
79125
required=dictionary["required"],
80126
options=[Option.from_dict(o) for o in dictionary["options"]],
81-
schema_id=dictionary["schemaNodeId"],
82-
feature_schema_id=dictionary["schemaNodeId"])
127+
schema_id=dictionary.get("schemaNodeId", []),
128+
feature_schema_id=dictionary.get("featureSchemaId", []))
83129

84130
def asdict(self) -> Dict[str, Any]:
85131
if self.class_type in Classification._REQUIRES_OPTIONS \
@@ -106,6 +152,34 @@ def add_option(self, option: Option):
106152

107153
@dataclass
108154
class Tool:
155+
"""
156+
A tool to be added to a Project's ontology. The tool is
157+
dependent on the Tool Type.
158+
159+
To instantiate, the "tool" and "name" parameters must
160+
be passed in.
161+
162+
The "classifications" parameter holds a list of Classification objects.
163+
This can be used to add nested classifications to a tool.
164+
165+
Example(s):
166+
tool = Tool(
167+
tool = Tool.Type.LINE,
168+
name = "Tool example")
169+
classification = Classification(
170+
class_type = Classification.Type.TEXT,
171+
instructions = "Classification Example")
172+
tool.add_classification(classification)
173+
174+
Attributes:
175+
tool: (Tool.Type)
176+
name: (str)
177+
required: (bool)
178+
color: (str)
179+
classifications: (list)
180+
schema_id: (str)
181+
feature_schema_id: (str)
182+
"""
109183
class Type(Enum):
110184
POLYGON = "polygon"
111185
SEGMENTATION = "superpixel"
@@ -125,8 +199,8 @@ class Type(Enum):
125199
@classmethod
126200
def from_dict(cls, dictionary: Dict[str, Any]):
127201
return Tool(name=dictionary['name'],
128-
schema_id=dictionary["schemaNodeId"],
129-
feature_schema_id=dictionary["featureSchemaId"],
202+
schema_id=dictionary.get("schemaNodeId", []),
203+
feature_schema_id=dictionary.get("featureSchemaId", []),
130204
required=dictionary["required"],
131205
tool=Tool.Type(dictionary["tool"]),
132206
classifications=[
@@ -194,7 +268,7 @@ def tools(self) -> List[Tool]:
194268
self._tools = [
195269
Tool.from_dict(tool) for tool in self.normalized['tools']
196270
]
197-
return self._tools # type: ignore
271+
return self._tools
198272

199273
def classifications(self) -> List[Classification]:
200274
"""Get list of classifications in an Ontology."""
@@ -203,24 +277,34 @@ def classifications(self) -> List[Classification]:
203277
Classification.from_dict(classification)
204278
for classification in self.normalized['classifications']
205279
]
206-
return self._classifications # type: ignore
207-
208-
209-
def convert_keys(json_dict: Dict[str, Any],
210-
converter: Callable) -> Dict[str, Any]:
211-
if isinstance(json_dict, dict):
212-
return {
213-
converter(key): convert_keys(value, converter)
214-
for key, value in json_dict.items()
215-
}
216-
if isinstance(json_dict, list):
217-
return [convert_keys(ele, converter) for ele in json_dict]
218-
return json_dict
280+
return self._classifications
219281

220282

221283
@dataclass
222284
class OntologyBuilder:
285+
"""
286+
A class to help create an ontology for a Project. This should be used
287+
for making Project ontologies from scratch. OntologyBuilder can also
288+
pull from an already existing Project's ontology.
223289
290+
There are no required instantiation arguments.
291+
292+
To create an ontology, use the asdict() method after fully building your
293+
ontology within this class, and inserting it into project.setup() as the
294+
"labeling_frontend_options" parameter.
295+
296+
Example:
297+
builder = OntologyBuilder()
298+
...
299+
frontend = list(client.get_labeling_frontends())[0]
300+
project.setup(frontend, builder.asdict())
301+
302+
attributes:
303+
tools: (list)
304+
classifications: (list)
305+
306+
307+
"""
224308
tools: List[Tool] = field(default_factory=list)
225309
classifications: List[Classification] = field(default_factory=list)
226310

@@ -234,11 +318,22 @@ def from_dict(cls, dictionary: Dict[str, Any]):
234318
])
235319

236320
def asdict(self):
321+
self._update_colors()
237322
return {
238323
"tools": [t.asdict() for t in self.tools],
239324
"classifications": [c.asdict() for c in self.classifications]
240325
}
241326

327+
def _update_colors(self):
328+
num_tools = len(self.tools)
329+
330+
for index in range(num_tools):
331+
hsv_color = (index * 1 / num_tools, 1, 1)
332+
rgb_color = tuple(
333+
int(255 * x) for x in colorsys.hsv_to_rgb(*hsv_color))
334+
if self.tools[index].color is None:
335+
self.tools[index].color = '#%02x%02x%02x' % rgb_color
336+
242337
@classmethod
243338
def from_project(cls, project: Project):
244339
ontology = project.ontology().normalized
@@ -257,4 +352,4 @@ def add_classification(self,
257352
raise InconsistentOntologyException(
258353
f"Duplicate classification instructions '{classification.instructions}'. "
259354
)
260-
self.classifications.append(classification)
355+
self.classifications.append(classification)

0 commit comments

Comments
 (0)