OntologyStructure
class encord.objects.ontology_labels_impl.OntologyStructure(objects=<factory>, classifications=<factory>)
objects: List[encord.objects.ontology_object.Object]
classifications: List[encord.objects.classification.Classification]
get_child_by_hash
Returns the first child node of this ontology tree node with the matching feature node hash. If there is more than one child with the same feature node hash in the ontology tree node, then the ontology would be in an invalid state. Throws if nothing is found or if the type is not matched.
get_child_by_hash(feature_node_hash, type_=None)
Parameters:
-
feature_node_hash (str) – the feature_node_hash of the child node to search for in the ontology.
-
type – The expected type of the item. If the found child does not match the type, an error will be thrown.
Return type:
[OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.
def get_child_by_hash(
self,
feature_node_hash: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> OntologyElementT:
"""
Returns the first child node of this ontology tree node with the matching feature node hash. If there is
more than one child with the same feature node hash in the ontology tree node, then the ontology would be in
an invalid state. Throws if nothing is found or if the type is not matched.
Args:
feature_node_hash: the feature_node_hash of the child node to search for in the ontology.
type_: The expected type of the item. If the found child does not match the type, an error will be thrown.
"""
for object_ in self.objects:
if object_.feature_node_hash == feature_node_hash:
return checked_cast(object_, type_)
found_item = _get_element_by_hash(feature_node_hash, object_.attributes)
if found_item is not None:
return checked_cast(found_item, type_)
for classification in self.classifications:
if classification.feature_node_hash == feature_node_hash:
return checked_cast(classification, type_)
found_item = _get_element_by_hash(feature_node_hash, classification.attributes)
if found_item is not None:
return checked_cast(found_item, type_)
raise OntologyError(f"Item not found: can't find an item with a hash {feature_node_hash} in the ontology.")
get_child_by_title
Returns a child node of this ontology tree node with the matching title and matching type if specified. If more than one child in this Object have the same title, then an error will be thrown. If no item is found, an error will be thrown as well.
get_child_by_title(title, type_=None)
Parameters:
-
title (str) – The exact title of the child node to search for in the ontology.
-
type – The expected type of the child node. Only a node that matches this type will be returned.
Return type:
[OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.
def get_child_by_title(
self,
title: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> OntologyElementT:
"""
Returns a child node of this ontology tree node with the matching title and matching type if specified. If more
than one child in this Object have the same title, then an error will be thrown. If no item is found, an error
will be thrown as well.
Args:
title: The exact title of the child node to search for in the ontology.
type_: The expected type of the child node. Only a node that matches this type will be returned.
"""
found_items = self.get_children_by_title(title, type_)
_assert_singular_result_list(found_items, title, type_)
return found_items[0]
get_children_by_title
Returns all the child nodes of this ontology tree node with the matching title and matching type if specified. Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.
get_children_by_title(title, type_=None)
Parameters:
-
title (str) – The exact title of the child node to search for in the ontology.
-
type – The expected type of the item. Only nodes that match this type will be returned.
Return type:
List[~OntologyElementT] - Returns all the child nodes of this Ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.
def get_children_by_title(
self,
title: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> List[OntologyElementT]:
"""
Returns all the child nodes of this ontology tree node with the matching title and matching type if specified.
Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.
Args:
title: The exact title of the child node to search for in the ontology.
type_: The expected type of the item. Only nodes that match this type will be returned.
"""
ret: List[OntologyElement] = []
for object_ in self.objects:
if object_.title == title and does_type_match(object_, type_):
ret.append(object_)
if type_ is None or issubclass(type_, OntologyNestedElement):
found_items = object_.get_children_by_title(title, type_=type_)
ret.extend(found_items)
for classification in self.classifications:
if classification.title == title and does_type_match(classification, type_):
ret.append(classification)
if type_ is None or issubclass(type_, OntologyNestedElement):
found_items = classification.get_children_by_title(title, type_=type_)
ret.extend(found_items)
# type checks in the code above guarantee the type conformity of the return value
# but there is no obvious way to tell that to mypy, so just casting here for now
return cast(List[OntologyElementT], ret)
from_dict
classmethod from_dict(d)
Parameters:
d (Dict[str, Any]) – a JSON blob of an “ontology structure” (for example, from the Encord web app)
Return type:
Raises:
KeyError – If the dict is missing a required field.
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> OntologyStructure:
"""
Args:
d: a JSON blob of an "ontology structure" (e.g. from Encord web app)
Raises:
KeyError: If the dict is missing a required field.
"""
objects_ret = [Object.from_dict(object_dict) for object_dict in d["objects"]]
classifications_ret = [
Classification.from_dict(classification_dict) for classification_dict in d["classifications"]
]
return OntologyStructure(objects=objects_ret, classifications=classifications_ret)
to_dict
to_dict()
Return type:
Dict[str, List[Dict[str, Any]]]
Returns:
The dict equivalent to the Ontology.
Raises:
KeyError – If the dict is missing a required field.
def to_dict(self) -> Dict[str, List[Dict[str, Any]]]:
"""
Returns:
The dict equivalent to the ontology.
Raises:
KeyError: If the dict is missing a required field.
"""
ret: Dict[str, List[Dict[str, Any]]] = dict()
ontology_objects: List[Dict[str, Any]] = list()
ret["objects"] = ontology_objects
for ontology_object in self.objects:
ontology_objects.append(ontology_object.to_dict())
ontology_classifications: List[Dict[str, Any]] = list()
ret["classifications"] = ontology_classifications
for ontology_classification in self.classifications:
ontology_classifications.append(ontology_classification.to_dict())
return ret
add_object
Adds an object class definition to the structure.
add_object(name, shape, uid=None, color=None, feature_node_hash=None)
Example:
from encord.objects.common import ChecklistAttribute, Shape
from encord.objects.ontology_structure import OntologyStructure
structure = OntologyStructure()
eye = structure.add_object(
name="Eye",
shape=Shape.POLYGON
)
nose = structure.add_object(
name="Nose",
shape=Shape.BOUNDING_BOX
)
nose_detail = nose.add_attribute(
ChecklistAttribute,
name='Checklist questions'
)
nose_detail.add_option(feature_node_hash="2bc17c88", label="A")
nose_detail.add_option(feature_node_hash="86eaa4f2", label="B ")
Parameters:
-
name (str) – The user-visible name of the object
-
shape (Shape) – The kind of object (bounding box, polygon, etc). See encord.objects.common.Shape for possible values
-
uid (Optional[int]) – Integer identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure
-
color (Optional[str]) – The color of the object in the label editor. Normally auto-assigned, should be in ‘#1A2B3F’ syntax.
-
feature_node_hash (Optional[str]) – Global identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure
Return type:
[Object]
Returns:
The created object class that can be further customized with attributes.
def add_object(
self,
name: str,
shape: Shape,
uid: Optional[int] = None,
color: Optional[str] = None,
feature_node_hash: Optional[str] = None,
) -> Object:
"""
Adds an object class definition to the structure.
.. code::
structure = ontology_structure.OntologyStructure()
eye = structure.add_object(
name="Eye",
)
nose = structure.add_object(
name="Nose",
)
nose_detail = nose.add_attribute(
encord.objects.common.ChecklistAttribute,
)
nose_detail.add_option(feature_node_hash="2bc17c88", label="Is it a cute nose?")
nose_detail.add_option(feature_node_hash="86eaa4f2", label="Is it a wet nose? ")
Args:
name: the user-visible name of the object
shape: the kind of object (bounding box, polygon, etc). See :py:class:`encord.objects.common.Shape` enum for possible values
uid: integer identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
color: the color of the object in the label editor. Normally auto-assigned, should be in '#1A2B3F' syntax.
feature_node_hash: global identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
Returns:
the created object class that can be further customized with attributes.
"""
if uid is None:
if self.objects:
uid = max([obj.uid for obj in self.objects]) + 1
else:
uid = 1
else:
if any([obj.uid == uid for obj in self.objects]):
raise ValueError(f"Duplicate uid '{uid}'")
if color is None:
color_index = 0
if self.objects:
try:
color_index = AVAILABLE_COLORS.index(self.objects[-1].color) + 1
if color_index >= len(AVAILABLE_COLORS):
color_index = 0
except ValueError:
pass
color = AVAILABLE_COLORS[color_index]
if feature_node_hash is None:
feature_node_hash = str(uuid4())[:8]
if any([obj.feature_node_hash == feature_node_hash for obj in self.objects]):
raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")
obj = Object(uid=uid, name=name, color=color, shape=shape, feature_node_hash=feature_node_hash)
self.objects.append(obj)
return obj
add_classification
Adds an classification definition to the ontology.
add_classification(uid=None, feature_node_hash=None)
Example
structure = ontology_structure.OntologyStructure()
cls = structure.add_classification(feature_node_hash="a39d81c0")
cat_standing = cls.add_attribute(
encord.objects.common.RadioAttribute,
feature_node_hash="a6136d14",
name="Is the cat standing?",
required=True,
)
cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
cat_standing.add_option(feature_node_hash="d0a4b373", label="No")
Parameters:
-
uid (Optional[int]) – Integer identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure
-
feature_node_hash (Optional[str]) – Global identifier of the object. Normally auto-generated. Omit this unless the aim is to create an exact clone of existing structure
Return type:
Returns:
The created Classification node.
Note
The Classification attribute should be further specified by calling its add_attribute() method.
def add_classification(
self,
uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
) -> Classification:
"""
Adds an classification definition to the ontology.
.. code::
structure = ontology_structure.OntologyStructure()
cls = structure.add_classification(feature_node_hash="a39d81c0")
cat_standing = cls.add_attribute(
encord.objects.common.RadioAttribute,
feature_node_hash="a6136d14",
name="Is the cat standing?",
required=True,
)
cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
cat_standing.add_option(feature_node_hash="d0a4b373", label="No")
Args:
uid: integer identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
feature_node_hash: global identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
Returns:
the created classification node. Note that classification attribute should be further specified by calling its `add_attribute()` method.
"""
if uid is None:
if self.classifications:
uid = max([cls.uid for cls in self.classifications]) + 1
else:
uid = 1
else:
if any([cls.uid == uid for cls in self.classifications]):
raise ValueError(f"Duplicate uid '{uid}'")
if feature_node_hash is None:
feature_node_hash = str(uuid4())[:8]
if any([cls.feature_node_hash == feature_node_hash for cls in self.classifications]):
raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")
cls = Classification(uid=uid, feature_node_hash=feature_node_hash, attributes=list())
self.classifications.append(cls)
return cls
Object
class encord.objects.ontology_labels_impl.Object(feature_node_hash, uid, name, color, shape, attributes=<factory>)
- uid: int
- name: str
- color: str
- shape: [encord.objects.common.Shape]
- feature_node_hash: str
- attributes: List[encord.objects.attributes.Attribute]
property title: str
Return Type:
str
property children: Sequence[encord.objects.ontology_element.OntologyElement]
Return Type:
Sequence[OntologyElement] - Returns all the child nodes of this ontology tree node with the matching title and matching type if specified. Titles in Ontologies do not need to be unique, however, we recommend unique titles when creating Ontologies.
create_instance
Create a encord.objects.ObjectInstance to be used with a label row.
create_instance()
Return type:
def create_instance(self) -> ObjectInstance:
"""Create a :class:`encord.objects.ObjectInstance` to be used with a label row."""
return ObjectInstance(self)
from_dict
classmethod from_dict(d)
Return type:
[Object]
@classmethod
def from_dict(cls, d: dict) -> Object:
shape_opt = Shape.from_string(d["shape"])
if shape_opt is None:
raise TypeError(f"The shape '{d['shape']}' of the object '{d}' is not recognised")
attributes_ret: List[Attribute] = [
attribute_from_dict(attribute_dict) for attribute_dict in d.get("attributes", [])
]
return Object(
uid=int(d["id"]),
name=d["name"],
color=d["color"],
shape=shape_opt,
feature_node_hash=d["featureNodeHash"],
attributes=attributes_ret,
)
to_dict
to_dict()
Return type:
Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {
"id": str(self.uid),
"name": self.name,
"color": self.color,
"shape": self.shape.value,
"featureNodeHash": self.feature_node_hash,
}
if attributes_list := attributes_to_list_dict(self.attributes):
ret["attributes"] = attributes_list
return ret
T
alias of TypeVar(‘T’, bound=encord.objects.attributes.Attribute)
T = TypeVar("T", bound=Attribute)
add_attribute
Adds an attribute to the object.
add_attribute(cls, name, local_uid=None, feature_node_hash=None, required=False, dynamic=False)
Parameters:
-
cls (Type[T]) – attribute type, one of RadioAttribute, ChecklistAttribute, TextAttribute
-
name (str) – the user-visible name of the attribute
-
local_uid (Optional[int]) – integer identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology
-
feature_node_hash (Optional[str]) – global identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology
-
required (bool) – whether the label editor would mark this attribute as ‘required’
-
dynamic (bool) – whether the attribute can have a different answer for the same object across different frames.
Return type:
T
Returns:
The created attribute that can be further specified with Options, where appropriate
Raises:
ValueError – if specified local_uid or feature_node_hash violate uniqueness constraints
def add_attribute(
self,
cls: Type[T],
name: str,
local_uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
required: bool = False,
dynamic: bool = False,
) -> T:
"""
Adds an attribute to the object.
Args:
cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
name: the user-visible name of the attribute
local_uid: integer identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
feature_node_hash: global identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
required: whether the label editor would mark this attribute as 'required'
dynamic: whether the attribute can have a different answer for the same object across different frames.
Returns:
the created attribute that can be further specified with Options, where appropriate
Raises:
ValueError: if specified `local_uid` or `feature_node_hash` violate uniqueness constraints
"""
return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required, dynamic)
Source
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar
from encord.objects.attributes import (
Attribute,
_add_attribute,
attribute_from_dict,
attributes_to_list_dict,
)
from encord.objects.common import Shape
from encord.objects.ontology_element import OntologyElement
@dataclass
class Object(OntologyElement):
uid: int
name: str
color: str
shape: Shape
feature_node_hash: str
attributes: List[Attribute] = field(default_factory=list)
@property
def title(self) -> str:
return self.name
@property
def children(self) -> Sequence[OntologyElement]:
return self.attributes
def create_instance(self) -> ObjectInstance:
"""Create a :class:`encord.objects.ObjectInstance` to be used with a label row."""
return ObjectInstance(self)
@classmethod
def from_dict(cls, d: dict) -> Object:
shape_opt = Shape.from_string(d["shape"])
if shape_opt is None:
raise TypeError(f"The shape '{d['shape']}' of the object '{d}' is not recognised")
attributes_ret: List[Attribute] = [
attribute_from_dict(attribute_dict) for attribute_dict in d.get("attributes", [])
]
return Object(
uid=int(d["id"]),
name=d["name"],
color=d["color"],
shape=shape_opt,
feature_node_hash=d["featureNodeHash"],
attributes=attributes_ret,
)
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {
"id": str(self.uid),
"name": self.name,
"color": self.color,
"shape": self.shape.value,
"featureNodeHash": self.feature_node_hash,
}
if attributes_list := attributes_to_list_dict(self.attributes):
ret["attributes"] = attributes_list
return ret
T = TypeVar("T", bound=Attribute)
def add_attribute(
self,
cls: Type[T],
name: str,
local_uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
required: bool = False,
dynamic: bool = False,
) -> T:
"""
Adds an attribute to the object.
Args:
cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
name: the user-visible name of the attribute
local_uid: integer identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
feature_node_hash: global identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
required: whether the label editor would mark this attribute as 'required'
dynamic: whether the attribute can have a different answer for the same object across different frames.
Returns:
the created attribute that can be further specified with Options, where appropriate
Raises:
ValueError: if specified `local_uid` or `feature_node_hash` violate uniqueness constraints
"""
return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required, dynamic)
from encord.objects.ontology_object_instance import ObjectInstance
Classification
Represents a whole-image classification as part of Ontology structure. Wraps a single Attribute that describes the image in general rather than an individual object.
class encord.objects.ontology_labels_impl.Classification(feature_node_hash, uid, attributes)
- uid: int
- feature_node_hash: str
- attributes: List[encord.objects.attributes.Attribute]
- property title: str
- Return type: str
- property children: Sequence[encord.objects.ontology_element.OntologyElement]
- Return type: Sequence[OntologyElement]
create_instance
Create a encord.objects.ClassificationInstance to be used with a label row.
create_instance()
Return type:
def create_instance(self) -> ClassificationInstance:
"""Create a :class:`encord.objects.ClassificationInstance` to be used with a label row."""
return ClassificationInstance(self)
from_dict
classmethod from_dict(d)
Return type:
@classmethod
def from_dict(cls, d: dict) -> Classification:
attributes_ret: List[Attribute] = [attribute_from_dict(attribute_dict) for attribute_dict in d["attributes"]]
return Classification(
uid=int(d["id"]),
feature_node_hash=d["featureNodeHash"],
attributes=attributes_ret,
)
T
alias of TypeVar(‘T’, bound=encord.objects.attributes.Attribute)
T = TypeVar("T", bound=Attribute)
add_attribute
Adds an attribute to the classification.
add_attribute(cls, name, local_uid=None, feature_node_hash=None, required=False)
Paramters:
-
cls (Type[T]) – attribute type, one of RadioAttribute, ChecklistAttribute, TextAttribute
-
name (str) – the user-visible name of the attribute
-
local_uid (Optional[int]) – integer identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology
-
feature_node_hash (Optional[str]) – global identifier of the attribute. Normally auto-generated; omit this unless the aim is to create an exact clone of existing ontology
-
required (bool) – whether the label editor would mark this attribute as ‘required’
Return type:
T
Returns:
The created attribute that can be further specified with Options, where appropriate
Raises:
ValueError – if the classification already has an attribute assigned
def add_attribute(
self,
cls: Type[T],
name: str,
local_uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
required: bool = False,
) -> T:
"""
Adds an attribute to the classification.
Args:
cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
name: the user-visible name of the attribute
local_uid: integer identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
feature_node_hash: global identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
required: whether the label editor would mark this attribute as 'required'
Returns:
the created attribute that can be further specified with Options, where appropriate
Raises:
ValueError: if the classification already has an attribute assigned
"""
if self.attributes:
raise ValueError("Classification should have exactly one root attribute")
return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required)
Source
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar
from encord.objects.attributes import (
Attribute,
_add_attribute,
attribute_from_dict,
attributes_to_list_dict,
)
from encord.objects.ontology_element import OntologyElement
@dataclass
class Classification(OntologyElement):
"""
Represents a whole-image classification as part of Ontology structure. Wraps a single Attribute that describes
the image in general rather than an individual object.
"""
uid: int
feature_node_hash: str
attributes: List[Attribute]
@property
def title(self) -> str:
return self.attributes[0].name
@property
def children(self) -> Sequence[OntologyElement]:
return self.attributes
def create_instance(self) -> ClassificationInstance:
"""Create a :class:`encord.objects.ClassificationInstance` to be used with a label row."""
return ClassificationInstance(self)
@classmethod
def from_dict(cls, d: dict) -> Classification:
attributes_ret: List[Attribute] = [attribute_from_dict(attribute_dict) for attribute_dict in d["attributes"]]
return Classification(
uid=int(d["id"]),
feature_node_hash=d["featureNodeHash"],
attributes=attributes_ret,
)
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {
"id": str(self.uid),
"featureNodeHash": self.feature_node_hash,
}
if attributes_list := attributes_to_list_dict(self.attributes):
ret["attributes"] = attributes_list
return ret
T = TypeVar("T", bound=Attribute)
def add_attribute(
self,
cls: Type[T],
name: str,
local_uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
required: bool = False,
) -> T:
"""
Adds an attribute to the classification.
Args:
cls: attribute type, one of `RadioAttribute`, `ChecklistAttribute`, `TextAttribute`
name: the user-visible name of the attribute
local_uid: integer identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
feature_node_hash: global identifier of the attribute. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing ontology
required: whether the label editor would mark this attribute as 'required'
Returns:
the created attribute that can be further specified with Options, where appropriate
Raises:
ValueError: if the classification already has an attribute assigned
"""
if self.attributes:
raise ValueError("Classification should have exactly one root attribute")
return _add_attribute(self.attributes, cls, name, [self.uid], local_uid, feature_node_hash, required)
def __hash__(self):
return hash(self.feature_node_hash)
from encord.objects.classification_instance import ClassificationInstance
Source
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Type, cast
from uuid import uuid4
from encord.exceptions import OntologyError
from encord.objects.classification import Classification
from encord.objects.common import Shape
from encord.objects.constants import AVAILABLE_COLORS
from encord.objects.ontology_element import (
OntologyElement,
OntologyElementT,
OntologyNestedElement,
_assert_singular_result_list,
_get_element_by_hash,
)
from encord.objects.ontology_object import Object
from encord.objects.utils import checked_cast, does_type_match
[docs]@dataclass
class OntologyStructure:
objects: List[Object] = field(default_factory=list)
classifications: List[Classification] = field(default_factory=list)
def get_child_by_hash(
self,
feature_node_hash: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> OntologyElementT:
"""
Returns the first child node of this ontology tree node with the matching feature node hash. If there is
more than one child with the same feature node hash in the ontology tree node, then the ontology would be in
an invalid state. Throws if nothing is found or if the type is not matched.
Args:
feature_node_hash: the feature_node_hash of the child node to search for in the ontology.
type_: The expected type of the item. If the found child does not match the type, an error will be thrown.
"""
for object_ in self.objects:
if object_.feature_node_hash == feature_node_hash:
return checked_cast(object_, type_)
found_item = _get_element_by_hash(feature_node_hash, object_.attributes)
if found_item is not None:
return checked_cast(found_item, type_)
for classification in self.classifications:
if classification.feature_node_hash == feature_node_hash:
return checked_cast(classification, type_)
found_item = _get_element_by_hash(feature_node_hash, classification.attributes)
if found_item is not None:
return checked_cast(found_item, type_)
raise OntologyError(f"Item not found: can't find an item with a hash {feature_node_hash} in the ontology.")
def get_child_by_title(
self,
title: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> OntologyElementT:
"""
Returns a child node of this ontology tree node with the matching title and matching type if specified. If more
than one child in this Object have the same title, then an error will be thrown. If no item is found, an error
will be thrown as well.
Args:
title: The exact title of the child node to search for in the ontology.
type_: The expected type of the child node. Only a node that matches this type will be returned.
"""
found_items = self.get_children_by_title(title, type_)
_assert_singular_result_list(found_items, title, type_)
return found_items[0]
def get_children_by_title(
self,
title: str,
type_: Optional[Type[OntologyElementT]] = None,
) -> List[OntologyElementT]:
"""
Returns all the child nodes of this ontology tree node with the matching title and matching type if specified.
Title in ontologies do not need to be unique, however, we recommend unique titles when creating ontologies.
Args:
title: The exact title of the child node to search for in the ontology.
type_: The expected type of the item. Only nodes that match this type will be returned.
"""
ret: List[OntologyElement] = []
for object_ in self.objects:
if object_.title == title and does_type_match(object_, type_):
ret.append(object_)
if type_ is None or issubclass(type_, OntologyNestedElement):
found_items = object_.get_children_by_title(title, type_=type_)
ret.extend(found_items)
for classification in self.classifications:
if classification.title == title and does_type_match(classification, type_):
ret.append(classification)
if type_ is None or issubclass(type_, OntologyNestedElement):
found_items = classification.get_children_by_title(title, type_=type_)
ret.extend(found_items)
# type checks in the code above guarantee the type conformity of the return value
# but there is no obvious way to tell that to mypy, so just casting here for now
return cast(List[OntologyElementT], ret)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> OntologyStructure:
"""
Args:
d: a JSON blob of an "ontology structure" (e.g. from Encord web app)
Raises:
KeyError: If the dict is missing a required field.
"""
objects_ret = [Object.from_dict(object_dict) for object_dict in d["objects"]]
classifications_ret = [
Classification.from_dict(classification_dict) for classification_dict in d["classifications"]
]
return OntologyStructure(objects=objects_ret, classifications=classifications_ret)
def to_dict(self) -> Dict[str, List[Dict[str, Any]]]:
"""
Returns:
The dict equivalent to the ontology.
Raises:
KeyError: If the dict is missing a required field.
"""
ret: Dict[str, List[Dict[str, Any]]] = dict()
ontology_objects: List[Dict[str, Any]] = list()
ret["objects"] = ontology_objects
for ontology_object in self.objects:
ontology_objects.append(ontology_object.to_dict())
ontology_classifications: List[Dict[str, Any]] = list()
ret["classifications"] = ontology_classifications
for ontology_classification in self.classifications:
ontology_classifications.append(ontology_classification.to_dict())
return ret
def add_object(
self,
name: str,
shape: Shape,
uid: Optional[int] = None,
color: Optional[str] = None,
feature_node_hash: Optional[str] = None,
) -> Object:
"""
Adds an object class definition to the structure.
.. code::
structure = ontology_structure.OntologyStructure()
eye = structure.add_object(
name="Eye",
)
nose = structure.add_object(
name="Nose",
)
nose_detail = nose.add_attribute(
encord.objects.common.ChecklistAttribute,
)
nose_detail.add_option(feature_node_hash="2bc17c88", label="Is it a cute nose?")
nose_detail.add_option(feature_node_hash="86eaa4f2", label="Is it a wet nose? ")
Args:
name: the user-visible name of the object
shape: the kind of object (bounding box, polygon, etc). See :py:class:`encord.objects.common.Shape` enum for possible values
uid: integer identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
color: the color of the object in the label editor. Normally auto-assigned, should be in '#1A2B3F' syntax.
feature_node_hash: global identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
Returns:
the created object class that can be further customised with attributes.
"""
if uid is None:
if self.objects:
uid = max([obj.uid for obj in self.objects]) + 1
else:
uid = 1
else:
if any([obj.uid == uid for obj in self.objects]):
raise ValueError(f"Duplicate uid '{uid}'")
if color is None:
color_index = 0
if self.objects:
try:
color_index = AVAILABLE_COLORS.index(self.objects[-1].color) + 1
if color_index >= len(AVAILABLE_COLORS):
color_index = 0
except ValueError:
pass
color = AVAILABLE_COLORS[color_index]
if feature_node_hash is None:
feature_node_hash = str(uuid4())[:8]
if any([obj.feature_node_hash == feature_node_hash for obj in self.objects]):
raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")
obj = Object(uid=uid, name=name, color=color, shape=shape, feature_node_hash=feature_node_hash)
self.objects.append(obj)
return obj
def add_classification(
self,
uid: Optional[int] = None,
feature_node_hash: Optional[str] = None,
) -> Classification:
"""
Adds an classification definition to the ontology.
.. code::
structure = ontology_structure.OntologyStructure()
cls = structure.add_classification(feature_node_hash="a39d81c0")
cat_standing = cls.add_attribute(
encord.objects.common.RadioAttribute,
feature_node_hash="a6136d14",
name="Is the cat standing?",
required=True,
)
cat_standing.add_option(feature_node_hash="a3aeb48d", label="Yes")
cat_standing.add_option(feature_node_hash="d0a4b373", label="No")
Args:
uid: integer identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
feature_node_hash: global identifier of the object. Normally auto-generated;
omit this unless the aim is to create an exact clone of existing structure
Returns:
the created classification node. Note that classification attribute should be further specified by calling its `add_attribute()` method.
"""
if uid is None:
if self.classifications:
uid = max([cls.uid for cls in self.classifications]) + 1
else:
uid = 1
else:
if any([cls.uid == uid for cls in self.classifications]):
raise ValueError(f"Duplicate uid '{uid}'")
if feature_node_hash is None:
feature_node_hash = str(uuid4())[:8]
if any([cls.feature_node_hash == feature_node_hash for cls in self.classifications]):
raise ValueError(f"Duplicate feature_node_hash '{feature_node_hash}'")
cls = Classification(uid=uid, feature_node_hash=feature_node_hash, attributes=list())
self.classifications.append(cls)
return cls