diff --git a/CHANGELOG.md b/CHANGELOG.md index 41db548..7986c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ Release - ```frame_data``` can only contain ```Num``` instances - ```object_data``` can not contain ```Num``` instances anymore - Major restructuring of the project directories -- ```FrameInterval.from_frame_uids()```: create ```FrameIntervals``` by providing a list of frame uids +- ```FrameInterval.from_frame_ids()```: create ```FrameIntervals``` by providing a list of frame uids - ```Object.object_data_pointers()```: generate ```ElementDataPointers``` - ```Scene.frame_intervals()```, ```Object.frame_intervals()```: generate ```FrameIntervals``` - ```Object.asdict()``` now provides also frame intervals and object data pointers, if the frames from the scene are provided @@ -109,3 +109,4 @@ Other breaking changes: - `raillabel.format.Transform` fields have been changed by `pos -> position` and `quad -> quaternion` to make it more explicit - `raillabel.format.FrameInterval` fields have been changed by `frame_start -> start` and `frame_end -> end` to make it more concise - `raillabel.format.Frame.uid` field has been removed to avoid redundant information +- `raillabel.format.Sensor` has been removed in favor of the different sensor type classes `raillabel.format.Camera`, `raillabel.format.Lidar`, `raillabel.format.Radar` and `raillabel.format.GpsImu` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b57e49..e04361d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,7 +138,7 @@ The key differences are: etc. - For classes that are not builtin (e.g. `Iterable`), `import collections.abc as cabc` and then use them like `cabc.Iterable`. - - Use [PEP-604-style unions], e.g. `int | float` instead of + - Use [PEP-604-style unions], e.g. `float` instead of `t.Union[int, float]`. - Use `... | None` (with `None` always as the last union member) instead of `t.Optional[...]` and always explicitly annotate where `None` is possible. diff --git a/pyproject.toml b/pyproject.toml index 7bda5fc..7f07378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,17 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -dependencies = ["jsonschema>=4.4.0", "fastjsonschema>=2.16.2", "pydantic<3.0.0"] +dependencies = [ + "pydantic<3.0.0", + "eval-type-backport==0.2.0" +] [project.urls] Homepage = "https://github.com/DSD-DBS/raillabel" Documentation = "https://dsd-dbs.github.io/raillabel" [project.optional-dependencies] -docs = ["furo", "sphinx", "sphinx-copybutton", "tomli; python_version<'3.11'"] +docs = ["furo", "sphinx", "sphinx-copybutton", "tomli; python_version<'3.14'"] test = ["pytest", "pytest-cov", "json5"] diff --git a/raillabel/format/__init__.py b/raillabel/format/__init__.py index 5761cc8..caab9fc 100644 --- a/raillabel/format/__init__.py +++ b/raillabel/format/__init__.py @@ -2,52 +2,55 @@ # SPDX-License-Identifier: Apache-2.0 """Module containing all relevant format classes.""" -from ._object_annotation import _ObjectAnnotation, annotation_classes from .bbox import Bbox +from .camera import Camera from .cuboid import Cuboid -from .element_data_pointer import ElementDataPointer from .frame import Frame from .frame_interval import FrameInterval +from .gps_imu import GpsImu from .intrinsics_pinhole import IntrinsicsPinhole from .intrinsics_radar import IntrinsicsRadar +from .lidar import Lidar from .metadata import Metadata from .num import Num from .object import Object +from .other_sensor import OtherSensor from .point2d import Point2d from .point3d import Point3d from .poly2d import Poly2d from .poly3d import Poly3d from .quaternion import Quaternion +from .radar import Radar from .scene import Scene from .seg3d import Seg3d -from .sensor import Sensor, SensorType from .sensor_reference import SensorReference from .size2d import Size2d from .size3d import Size3d from .transform import Transform __all__ = [ - "_ObjectAnnotation", - "annotation_classes", "Bbox", + "Camera", "Cuboid", "ElementDataPointer", "Frame", "FrameInterval", + "GpsImu", "IntrinsicsPinhole", "IntrinsicsRadar", + "Lidar", "Metadata", "Num", "Object", + "OtherSensor", "Point2d", "Point3d", "Poly2d", "Poly3d", "Quaternion", + "Radar", "Scene", "Seg3d", - "Sensor", - "SensorType", "SensorReference", "Size2d", "Size3d", diff --git a/raillabel/format/_attribute_type.py b/raillabel/format/_attribute_type.py deleted file mode 100644 index 526c5e2..0000000 --- a/raillabel/format/_attribute_type.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from enum import Enum - - -class AttributeType(Enum): - """Enum of all valid RailLabel attribute types.""" - - TEXT = "text" - NUM = "num" - BOOLEAN = "boolean" - VEC = "vec" - - @classmethod - def from_value(cls, attribute_value_class: type) -> AttributeType: - """Return AttributeType based on class of attribute value. - - Parameters - ---------- - attribute_value_class: type - Class of the attribute value. Can be gathered by calling type()-function. - - Returns - ------- - AttributeType - Corresponding AttributeType. - - Raises - ------ - UnsupportedAttributeTypeError - if attribute value class does not correspond to an Attribute Type. - - """ - if attribute_value_class is str: - return AttributeType.TEXT - - if attribute_value_class in [float, int]: - return AttributeType.NUM - - if attribute_value_class is bool: - return AttributeType.BOOLEAN - - if attribute_value_class in [list, tuple]: - return AttributeType.VEC - - raise UnsupportedAttributeTypeError(attribute_value_class) - - -class UnsupportedAttributeTypeError(ValueError): - def __init__(self, attribute_value_class: type) -> None: - super().__init__( - f"Type {attribute_value_class} does not correspond to a valid RailLabel attribute " - "type. Supported types are str, float, int, bool, list, tuple." - ) diff --git a/raillabel/format/_attributes.py b/raillabel/format/_attributes.py new file mode 100644 index 0000000..a0bd42c --- /dev/null +++ b/raillabel/format/_attributes.py @@ -0,0 +1,76 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from raillabel.json_format import ( + JSONAttributes, + JSONBooleanAttribute, + JSONNumAttribute, + JSONTextAttribute, + JSONVecAttribute, +) + + +def _attributes_from_json(json: JSONAttributes | None) -> dict[str, float | bool | str | list]: + """Parse the annotation attributes from json.""" + if json is None: + return {} + + attributes: dict[str, float | bool | str | list] = {} + + if json.boolean is not None: + for bool_attribute in json.boolean: + attributes[bool_attribute.name] = bool_attribute.val + + if json.num is not None: + for num_attribute in json.num: + attributes[num_attribute.name] = num_attribute.val + + if json.text is not None: + for text_attribute in json.text: + attributes[text_attribute.name] = text_attribute.val + + if json.vec is not None: + for vec_attribute in json.vec: + attributes[vec_attribute.name] = vec_attribute.val + + return attributes + + +def _attributes_to_json(attributes: dict[str, float | bool | str | list]) -> JSONAttributes | None: + if len(attributes) == 0: + return None + + boolean_attributes = [] + num_attributes = [] + text_attributes = [] + vec_attributes = [] + + for name, value in attributes.items(): + if isinstance(value, bool): + boolean_attributes.append(JSONBooleanAttribute(name=name, val=value)) + + elif isinstance(value, (int, float)): + num_attributes.append(JSONNumAttribute(name=name, val=value)) + + elif isinstance(value, str): + text_attributes.append(JSONTextAttribute(name=name, val=value)) + + elif isinstance(value, list): + vec_attributes.append(JSONVecAttribute(name=name, val=value)) + + else: + raise UnsupportedAttributeTypeError(name, value) + + return JSONAttributes( + boolean=boolean_attributes, num=num_attributes, text=text_attributes, vec=vec_attributes + ) + + +class UnsupportedAttributeTypeError(TypeError): + def __init__(self, attribute_name: str, attribute_value: object) -> None: + super().__init__( + f"{attribute_value.__class__.__name__} of attribute {attribute_name} " + "is not a supported attribute type" + ) diff --git a/raillabel/format/_object_annotation.py b/raillabel/format/_object_annotation.py deleted file mode 100644 index 4487cf2..0000000 --- a/raillabel/format/_object_annotation.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from abc import ABC, abstractmethod, abstractproperty -from dataclasses import dataclass -from importlib import import_module -from inspect import isclass -from pathlib import Path -from pkgutil import iter_modules -from typing import Any - -from ._attribute_type import AttributeType -from .object import Object -from .sensor import Sensor - - -@dataclass -class _ObjectAnnotation(ABC): - uid: str - object: Object - sensor: Sensor - attributes: dict[str, int | float | bool | str | list] - - @property - def name(self) -> str: - return f"{self.sensor.uid}__{self.OPENLABEL_ID}__{self.object.type}" - - @property - @abstractproperty - def OPENLABEL_ID(self) -> list[str] | str: - raise NotImplementedError - - # === Public Methods ===================================================== - - @abstractmethod - def asdict(self) -> dict: - raise NotImplementedError - - @classmethod - @abstractmethod - def fromdict( - cls, - data_dict: dict, - sensors: dict, - object: Object, - ) -> type[_ObjectAnnotation]: - raise NotImplementedError - - # === Private Methods ==================================================== - - def _annotation_required_fields_asdict(self) -> dict: - """Return the required fields from the parent class to dict.""" - return { - "uid": str(self.uid), - "name": str(self.name), - } - - def _annotation_optional_fields_asdict(self) -> dict[str, Any]: - """Return the optional fields from the parent class to dict.""" - dict_repr: dict[str, Any] = {} - - if self.sensor is not None: - dict_repr["coordinate_system"] = str(self.sensor.uid) - - if self.attributes != {}: - dict_repr["attributes"] = self._attributes_asdict(self.attributes) - - return dict_repr - - def _attributes_asdict(self, attributes: dict[str, Any]) -> dict[str, Any]: - attributes_dict: dict[str, Any] = {} - - for attr_name, attr_value in attributes.items(): - attr_type = AttributeType.from_value(type(attr_value)).value - - if attr_type not in attributes_dict: - attributes_dict[attr_type] = [] - - attributes_dict[attr_type].append({"name": attr_name, "val": attr_value}) - - return attributes_dict - - @classmethod - def _coordinate_system_fromdict(cls, data_dict: dict, sensors: dict) -> Sensor: - return sensors[data_dict["coordinate_system"]] - - @classmethod - def _attributes_fromdict( - cls, - data_dict: dict, - ) -> dict[str, int | float | bool | str | list]: - if "attributes" not in data_dict: - return {} - - return {a["name"]: a["val"] for type_ in data_dict["attributes"].values() for a in type_} - - -def annotation_classes() -> dict[str, type[_ObjectAnnotation]]: - """Return dictionary with _Annotation child classes.""" - out = {} - - package_dir = str(Path(__file__).resolve().parent) - for _, module_name, _ in iter_modules([package_dir]): - module = import_module(f"raillabel.format.{module_name}") - for attribute_name in dir(module): - attribute = getattr(module, attribute_name) - - if ( - isclass(attribute) - and issubclass(attribute, _ObjectAnnotation) - and attribute != _ObjectAnnotation - ): - out[attribute.OPENLABEL_ID] = attribute - - return out # type: ignore diff --git a/raillabel/format/_sensor_without_intrinsics.py b/raillabel/format/_sensor_without_intrinsics.py new file mode 100644 index 0000000..24522ff --- /dev/null +++ b/raillabel/format/_sensor_without_intrinsics.py @@ -0,0 +1,30 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from raillabel.json_format import JSONTransformData + +from .transform import Transform + + +@dataclass +class _SensorWithoutIntrinsics: + """Parent class of all sensors, that do not have an intrinsic calibration.""" + + extrinsics: Transform | None = None + "External calibration of the sensor defined by the 3D transform to the coordinate system origin." + + uri: str | None = None + "Name of the subdirectory containing the sensor files." + + description: str | None = None + "Additional information about the sensor." + + +def _extrinsics_from_json(json_transform: JSONTransformData | None) -> Transform | None: + if json_transform is None: + return None + return Transform.from_json(json_transform) diff --git a/raillabel/format/_util.py b/raillabel/format/_util.py new file mode 100644 index 0000000..02f5e57 --- /dev/null +++ b/raillabel/format/_util.py @@ -0,0 +1,16 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + + +def _empty_list_to_none(collection: list | None) -> list | None: + if collection is None: + return None + if len(collection) == 0: + return None + return collection + + +def _flatten_list(list_of_tuples: list[tuple]) -> list: + return [item for tup in list_of_tuples for item in tup] diff --git a/raillabel/format/bbox.py b/raillabel/format/bbox.py index 67406e8..cd58b41 100644 --- a/raillabel/format/bbox.py +++ b/raillabel/format/bbox.py @@ -4,96 +4,55 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from ._object_annotation import _ObjectAnnotation -from .object import Object +from raillabel.json_format import JSONBbox + +from ._attributes import _attributes_from_json, _attributes_to_json from .point2d import Point2d from .size2d import Size2d @dataclass -class Bbox(_ObjectAnnotation): - """A 2D bounding box in an image. - - Parameters - ---------- - uid: str - This a string representing the unique universal identifier of the annotation. - pos: raillabel.format.Point2d - The center point of the bbox in pixels. - size: raillabel.format.Size2d - The dimensions of the bbox in pixels from the top left corner to the bottom right corner. - object: raillabel.format.Object - A reference to the object, this annotation belongs to. - sensor: raillabel.format.Sensor - A reference to the sensor, this annotation is labeled in. Default is None. - attributes: dict, optional - Attributes of the annotation. Dict keys are the name str of the attribute, values are the - attribute values. Default is {}. - - Properties (read-only) - ---------------------- - name: str - Name of the annotation used by the VCD player for indexing in the object data pointers. - - """ +class Bbox: + """A 2D bounding box in an image.""" pos: Point2d - size: Size2d + "The center point of the bbox in pixels." - OPENLABEL_ID = "bbox" + size: Size2d + "The dimensions of the bbox in pixels from the top left corner to the bottom right corner." - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict, object: Object) -> Bbox: - """Generate a Bbox object from a dict. + object_id: UUID + "The unique identifyer of the real-life object, this annotation belongs to." - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. - object: raillabel.format.Object - Object this annotation belongs to. + sensor_id: str + "The unique identifyer of the sensor this annotation is labeled in." - Returns - ------- - annotation: Bbox - Converted annotation. + attributes: dict[str, float | bool | str | list] + "Additional information associated with the annotation." - """ + @classmethod + def from_json(cls, json: JSONBbox, object_id: UUID) -> Bbox: + """Construct an instant of this class from RailLabel JSON data.""" return Bbox( - uid=str(data_dict["uid"]), - pos=Point2d(x=data_dict["val"][0], y=data_dict["val"][1]), - size=Size2d(x=data_dict["val"][2], y=data_dict["val"][3]), - object=object, - sensor=cls._coordinate_system_fromdict(data_dict, sensors), - attributes=cls._attributes_fromdict(data_dict), + pos=Point2d.from_json((json.val[0], json.val[1])), + size=Size2d.from_json((json.val[2], json.val[3])), + object_id=object_id, + sensor_id=json.coordinate_system, + attributes=_attributes_from_json(json.attributes), ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr = self._annotation_required_fields_asdict() - - dict_repr["val"] = [ - float(self.pos.x), - float(self.pos.y), - float(self.size.x), - float(self.size.y), - ] - - dict_repr.update(self._annotation_optional_fields_asdict()) + def to_json(self, uid: UUID, object_type: str) -> JSONBbox: + """Export this object into the RailLabel JSON format.""" + return JSONBbox( + name=self.name(object_type), + val=list(self.pos.to_json()) + list(self.size.to_json()), + coordinate_system=self.sensor_id, + uid=uid, + attributes=_attributes_to_json(self.attributes), + ) - return dict_repr + def name(self, object_type: str) -> str: + """Return the name of the annotation used for indexing in the object data pointers.""" + return f"{self.sensor_id}__bbox__{object_type}" diff --git a/raillabel/format/camera.py b/raillabel/format/camera.py new file mode 100644 index 0000000..b692afe --- /dev/null +++ b/raillabel/format/camera.py @@ -0,0 +1,69 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from raillabel.json_format import ( + JSONCoordinateSystem, + JSONStreamCamera, + JSONStreamCameraProperties, + JSONTransformData, +) + +from .intrinsics_pinhole import IntrinsicsPinhole +from .transform import Transform + + +@dataclass +class Camera: + """A camera sensor.""" + + intrinsics: IntrinsicsPinhole + "The intrinsic calibration of the sensor." + + extrinsics: Transform | None = None + "External calibration of the sensor defined by the 3D transform to the coordinate system origin." + + uri: str | None = None + "Name of the subdirectory containing the sensor files." + + description: str | None = None + "Additional information about the sensor." + + @classmethod + def from_json( + cls, json_stream: JSONStreamCamera, json_coordinate_system: JSONCoordinateSystem + ) -> Camera: + """Construct an instant of this class from RailLabel JSON data.""" + return Camera( + intrinsics=IntrinsicsPinhole.from_json(json_stream.stream_properties.intrinsics_pinhole), + extrinsics=_extrinsics_from_json(json_coordinate_system.pose_wrt_parent), + uri=json_stream.uri, + description=json_stream.description, + ) + + def to_json(self) -> tuple[JSONStreamCamera, JSONCoordinateSystem]: + """Export this object into the RailLabel JSON format.""" + return ( + JSONStreamCamera( + type="camera", + stream_properties=JSONStreamCameraProperties( + intrinsics_pinhole=self.intrinsics.to_json() + ), + uri=self.uri, + description=self.description, + ), + JSONCoordinateSystem( + parent="base", + type="sensor", + pose_wrt_parent=self.extrinsics.to_json() if self.extrinsics is not None else None, + ), + ) + + +def _extrinsics_from_json(json_transform: JSONTransformData | None) -> Transform | None: + if json_transform is None: + return None + return Transform.from_json(json_transform) diff --git a/raillabel/format/cuboid.py b/raillabel/format/cuboid.py index 36bcf2d..227b9f9 100644 --- a/raillabel/format/cuboid.py +++ b/raillabel/format/cuboid.py @@ -4,121 +4,61 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from ._object_annotation import _ObjectAnnotation -from .object import Object +from raillabel.json_format import JSONCuboid + +from ._attributes import _attributes_from_json, _attributes_to_json from .point3d import Point3d from .quaternion import Quaternion from .size3d import Size3d @dataclass -class Cuboid(_ObjectAnnotation): - """3D bounding box. - - Parameters - ---------- - uid: str - This a string representing the unique universal identifier of the annotation. - pos: raillabel.format.Point3d - The center position of the cuboid in meters, where the x coordinate points ahead of the - vehicle, y points to the left and z points upwards. - quat: raillabel.format.Quaternion - The rotation of the cuboid in quaternions. - size: raillabel.format.Size3d - The size of the cuboid in meters. - object: raillabel.format.Object - A reference to the object, this annotation belongs to. - sensor: raillabel.format.Sensor - A reference to the sensor, this annotation is labeled in. Default is None. - attributes: dict, optional - Attributes of the annotation. Dict keys are the name str of the attribute, values are the - attribute values. Default is {}. - - Properties (read-only) - ---------------------- - name: str - Name of the annotation used by the VCD player for indexing in the object data pointers. - - """ +class Cuboid: + """3D bounding box.""" pos: Point3d + """The center position of the cuboid in meters, where the x coordinate points ahead of the + vehicle, y points to the left and z points upwards.""" + quat: Quaternion - size: Size3d + "The rotation of the cuboid in quaternions." - OPENLABEL_ID = "cuboid" + size: Size3d + "The size of the cuboid in meters." - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict, object: Object) -> Cuboid: - """Generate a Cuboid object from a dict. + object_id: UUID + "The unique identifyer of the real-life object, this annotation belongs to." - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. - object: raillabel.format.Object - Object this annotation belongs to. + sensor_id: str + "The unique identifyer of the sensor this annotation is labeled in." - Returns - ------- - annotation: Cuboid - Converted annotation. + attributes: dict[str, float | bool | str | list] + "Additional information associated with the annotation." - """ + @classmethod + def from_json(cls, json: JSONCuboid, object_id: UUID) -> Cuboid: + """Construct an instant of this class from RailLabel JSON data.""" return Cuboid( - uid=str(data_dict["uid"]), - pos=Point3d( - x=data_dict["val"][0], - y=data_dict["val"][1], - z=data_dict["val"][2], - ), - quat=Quaternion( - x=data_dict["val"][3], - y=data_dict["val"][4], - z=data_dict["val"][5], - w=data_dict["val"][6], - ), - size=Size3d( - x=data_dict["val"][7], - y=data_dict["val"][8], - z=data_dict["val"][9], - ), - object=object, - sensor=cls._coordinate_system_fromdict(data_dict, sensors), - attributes=cls._attributes_fromdict(data_dict), + pos=Point3d.from_json((json.val[0], json.val[1], json.val[2])), + quat=Quaternion.from_json((json.val[3], json.val[4], json.val[5], json.val[6])), + size=Size3d.from_json((json.val[7], json.val[8], json.val[9])), + object_id=object_id, + sensor_id=json.coordinate_system, + attributes=_attributes_from_json(json.attributes), ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr = self._annotation_required_fields_asdict() - - dict_repr["val"] = [ - float(self.pos.x), - float(self.pos.y), - float(self.pos.z), - float(self.quat.x), - float(self.quat.y), - float(self.quat.z), - float(self.quat.w), - float(self.size.x), - float(self.size.y), - float(self.size.z), - ] - - dict_repr.update(self._annotation_optional_fields_asdict()) + def to_json(self, uid: UUID, object_type: str) -> JSONCuboid: + """Export this object into the RailLabel JSON format.""" + return JSONCuboid( + name=self.name(object_type), + val=list(self.pos.to_json()) + list(self.quat.to_json()) + list(self.size.to_json()), + coordinate_system=self.sensor_id, + uid=uid, + attributes=_attributes_to_json(self.attributes), + ) - return dict_repr + def name(self, object_type: str) -> str: + """Return the name of the annotation used for indexing in the object data pointers.""" + return f"{self.sensor_id}__cuboid__{object_type}" diff --git a/raillabel/format/element_data_pointer.py b/raillabel/format/element_data_pointer.py deleted file mode 100644 index f2c3e07..0000000 --- a/raillabel/format/element_data_pointer.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - -from ._attribute_type import AttributeType -from .frame_interval import FrameInterval - - -@dataclass -class ElementDataPointer: - """Container of pointers for annotations indexed by the "name" property. - - Used for indexing annotations for easy referencing without loading all of them. - - Parameters - ---------- - uid: str - Unique identifier of the ElementDataPointer assembled using a specific schema. - frame_intervals: list[raillabel.format.FrameInterval] - Frame intervals, this element data pointer is contained in. - attribute_pointers: dict[str, raillabel.util._attribute_type.AttributeType] - References of attributes contained in the referenced annotations with attribute type. - - Properties (read-only) - ---------------------- - uid: str - Unique identifier of the ElementDataPointer built from the attributes. - - """ - - uid: str - frame_intervals: list[FrameInterval] - attribute_pointers: dict[str, AttributeType] - - @property - def annotation_type(self) -> str: - """Return type of annotation e.g. bbox, cuboid.""" - return self.uid.split("__")[1] - - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "type": self.annotation_type, - "frame_intervals": self._frame_intervals_asdict(), - "attribute_pointers": self._attribute_pointers_asdict(), - } - - def _frame_intervals_asdict(self) -> list[dict[str, int]]: - return [fi.asdict() for fi in self.frame_intervals] - - def _attribute_pointers_asdict(self) -> dict[str, str]: - return { - attr_name: attr_type.value for attr_name, attr_type in self.attribute_pointers.items() - } diff --git a/raillabel/format/frame.py b/raillabel/format/frame.py index 0dcd722..91fb7c3 100644 --- a/raillabel/format/frame.py +++ b/raillabel/format/frame.py @@ -3,219 +3,196 @@ from __future__ import annotations -import decimal -import typing as t from dataclasses import dataclass, field - -from ._object_annotation import _ObjectAnnotation, annotation_classes +from decimal import Decimal +from uuid import UUID + +from raillabel.json_format import ( + JSONAnnotations, + JSONFrame, + JSONFrameData, + JSONFrameProperties, + JSONObjectData, +) + +from ._util import _empty_list_to_none +from .bbox import Bbox +from .cuboid import Cuboid from .num import Num from .object import Object -from .sensor import Sensor +from .poly2d import Poly2d +from .poly3d import Poly3d +from .seg3d import Seg3d from .sensor_reference import SensorReference @dataclass class Frame: - """A container of dynamic, timewise, information. - - Parameters - ---------- - timestamp: decimal.Decimal, optional - Timestamp containing the Unix epoch time of the frame with up to nanosecond precision. - sensors: dict of raillabel.format.SensorReference, optional - References to the sensors with frame specific information like timestamp and uri. - Default is {}. - frame_data: dict, optional - Dictionary containing data directly connected to the frame and not to any object, like - gps/imu data. Dictionary keys are the ID-strings of the variable the data belongs to. - Default is {}. - annotations: dict[str, _ObjectAnnotation subclass], optional - Dictionary containing all annotations of this frame. Keys are annotation uids. - - Read-Only Attributes - -------------------- - object_data: dict[str, dict[str, _ObjectAnnotation subclass]] - Annotations categorized by object. Keys are object uids and values are the annotations - as a dict, that are part of the object. - - """ - - timestamp: decimal.Decimal | None = None - sensors: dict[str, SensorReference] = field(default_factory=dict) - frame_data: dict[str, Num] = field(default_factory=dict) - annotations: dict[str, type[_ObjectAnnotation]] = field(default_factory=dict) + """A container of dynamic, timewise, information.""" - @property - def object_data(self) -> dict[str, dict[str, type[_ObjectAnnotation]]]: - """Return annotations categorized by Object-Id. + timestamp: Decimal | None = None + "Timestamp containing the Unix epoch time of the frame with up to nanosecond precision." - Returns - ------- - dict[str, dict[UUID, _ObjectAnnotation subclass]] - Dictionary of annotations. Keys are object uids and values are annotations, that are - contained in the object. - - """ - object_data: dict[str, dict[str, type[_ObjectAnnotation]]] = {} - for ann_id, annotation in self.annotations.items(): - if annotation.object.uid not in object_data: - object_data[annotation.object.uid] = {} + sensors: dict[str, SensorReference] = field(default_factory=dict) + "References to the sensors with frame specific information like timestamp and uri." - object_data[annotation.object.uid][ann_id] = annotation + frame_data: dict[str, Num] = field(default_factory=dict) + """Dictionary containing data directly connected to the frame and not to any object, like + gps/imu data. Dictionary keys are the ID-strings of the variable the data belongs to.""" - return object_data + annotations: dict[UUID, Bbox | Cuboid | Poly2d | Poly3d | Seg3d] = field(default_factory=dict) + "All annotations of this frame." @classmethod - def fromdict( - cls, - data_dict: dict, - objects: dict[str, Object], - sensors: dict[str, Sensor], - ) -> Frame: - """Generate a Frame object from a dict. - - Parameters - ---------- - uid: str - Unique identifier of the frame. - data_dict: dict - RailLabel format snippet containing the relevant data. - objects: dict - Dictionary of all objects in the scene. - sensors: dict - Dictionary of all sensors in the scene. - - Returns - ------- - frame: raillabel.format.Frame - Converted Frame object. - - """ + def from_json(cls, json: JSONFrame) -> Frame: + """Construct an instant of this class from RailLabel JSON data.""" return Frame( - timestamp=cls._timestamp_fromdict(data_dict), - sensors=cls._sensors_fromdict(data_dict, sensors), - frame_data=cls._frame_data_fromdict(data_dict, sensors), - annotations=cls._objects_fromdict(data_dict, objects, sensors), + timestamp=_timestamp_from_dict(json.frame_properties), + sensors=_sensors_from_dict(json.frame_properties), + frame_data=_frame_data_from_dict(json.frame_properties), + annotations=_annotations_from_json(json.objects), ) - def asdict(self) -> dict[str, t.Any]: - """Export self as a dict compatible with the OpenLABEL schema. + def to_json(self, objects: dict[UUID, Object]) -> JSONFrame: + """Export this object into the RailLabel JSON format.""" + return JSONFrame( + frame_properties=JSONFrameProperties( + timestamp=self.timestamp, + streams={ + sensor_id: sensor_ref.to_json() for sensor_id, sensor_ref in self.sensors.items() + }, + frame_data=JSONFrameData(num=[num.to_json() for num in self.frame_data.values()]), + ), + objects=_objects_to_json(self.annotations, objects), + ) - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. +def _timestamp_from_dict(frame_properties: JSONFrameProperties | None) -> Decimal | None: + if frame_properties is None: + return None - """ - dict_repr: dict[str, t.Any] = {} + if frame_properties.timestamp is None: + return None - if self.timestamp is not None or self.sensors != {} or self.frame_data != {}: - dict_repr["frame_properties"] = {} + return Decimal(frame_properties.timestamp) - if self.timestamp is not None: - dict_repr["frame_properties"]["timestamp"] = str(self.timestamp) - if self.sensors != {}: - dict_repr["frame_properties"]["streams"] = { - str(k): v.asdict() for k, v in self.sensors.items() - } +def _sensors_from_dict(frame_properties: JSONFrameProperties | None) -> dict[str, SensorReference]: + if frame_properties is None: + return {} - if self.frame_data != {}: - dict_repr["frame_properties"]["frame_data"] = { - "num": [v.asdict() for v in self.frame_data.values()] - } + if frame_properties.streams is None: + return {} - if self.annotations != {}: - dict_repr["objects"] = self._annotations_asdict() + return { + sensor_id: SensorReference.from_json(sensor_ref) + for sensor_id, sensor_ref in frame_properties.streams.items() + } - return dict_repr - @classmethod - def _timestamp_fromdict(cls, data_dict: dict) -> decimal.Decimal | None: - if "frame_properties" not in data_dict or "timestamp" not in data_dict["frame_properties"]: - return None +def _frame_data_from_dict(frame_properties: JSONFrameProperties | None) -> dict[str, Num]: + if frame_properties is None: + return {} - return decimal.Decimal(data_dict["frame_properties"]["timestamp"]) + if frame_properties.frame_data is None: + return {} - @classmethod - def _sensors_fromdict( - cls, data_dict: dict, scene_sensors: dict[str, Sensor] - ) -> dict[str, SensorReference]: - if "frame_properties" not in data_dict or "streams" not in data_dict["frame_properties"]: - return {} + if frame_properties.frame_data.num is None: + return {} - sensors = {} + return {num.name: Num.from_json(num) for num in frame_properties.frame_data.num} - for sensor_id, sensor_dict in data_dict["frame_properties"]["streams"].items(): - sensors[sensor_id] = SensorReference.fromdict( - data_dict=sensor_dict, sensor=scene_sensors[sensor_id] - ) - return sensors +def _annotations_from_json( + json_object_data: dict[UUID, JSONObjectData] | None, +) -> dict[UUID, Bbox | Cuboid | Poly2d | Poly3d | Seg3d]: + if json_object_data is None: + return {} - @classmethod - def _frame_data_fromdict(cls, data_dict: dict, sensors: dict[str, Sensor]) -> dict[str, Num]: - if "frame_properties" not in data_dict or "frame_data" not in data_dict["frame_properties"]: - return {} + annotations: dict[UUID, Bbox | Cuboid | Poly2d | Poly3d | Seg3d] = {} - frame_data = {} - for ann_type in data_dict["frame_properties"]["frame_data"]: - for ann_raw in data_dict["frame_properties"]["frame_data"][ann_type]: - frame_data[ann_raw["name"]] = Num.fromdict(ann_raw, sensors) + for object_id, object_data in json_object_data.items(): + for json_bbox in _resolve_none_to_empty_list(object_data.object_data.bbox): + annotations[json_bbox.uid] = Bbox.from_json(json_bbox, object_id) - return frame_data + for json_cuboid in _resolve_none_to_empty_list(object_data.object_data.cuboid): + annotations[json_cuboid.uid] = Cuboid.from_json(json_cuboid, object_id) - @classmethod - def _objects_fromdict( - cls, - data_dict: dict, - objects: dict[str, Object], - sensors: dict[str, Sensor], - ) -> dict[str, type[_ObjectAnnotation]]: - if "objects" not in data_dict: - return {} - - annotations = {} - - for obj_id, obj_ann in data_dict["objects"].items(): - object_annotations = cls._object_annotations_fromdict( - data_dict=obj_ann["object_data"], - object=objects[obj_id], - sensors=sensors, - ) + for json_poly2d in _resolve_none_to_empty_list(object_data.object_data.poly2d): + annotations[json_poly2d.uid] = Poly2d.from_json(json_poly2d, object_id) - for annotation in object_annotations: - annotations[annotation.uid] = annotation + for json_poly3d in _resolve_none_to_empty_list(object_data.object_data.poly3d): + annotations[json_poly3d.uid] = Poly3d.from_json(json_poly3d, object_id) - return annotations + for json_seg3d in _resolve_none_to_empty_list(object_data.object_data.vec): + annotations[json_seg3d.uid] = Seg3d.from_json(json_seg3d, object_id) - @classmethod - def _object_annotations_fromdict( - cls, - data_dict: dict, - object: Object, - sensors: dict[str, Sensor], - ) -> t.Iterator[type[_ObjectAnnotation]]: - for ann_type, annotations_raw in data_dict.items(): - for ann_raw in annotations_raw: - yield annotation_classes()[ann_type].fromdict(ann_raw, sensors, object) - - def _annotations_asdict(self) -> dict[str, t.Any]: - annotations_dict: dict[str, t.Any] = {} - for object_id, annotations_ in self.object_data.items(): - annotations_dict[object_id] = {"object_data": {}} - - for annotation in annotations_.values(): - if annotation.OPENLABEL_ID not in annotations_dict[object_id]["object_data"]: - annotations_dict[object_id]["object_data"][annotation.OPENLABEL_ID] = [] - - annotations_dict[object_id]["object_data"][annotation.OPENLABEL_ID].append( - annotation.asdict() # type: ignore + return annotations + + +def _resolve_none_to_empty_list(optional_list: list | None) -> list: + if optional_list is None: + return [] + return optional_list + + +def _objects_to_json( + annotations: dict[UUID, Bbox | Cuboid | Poly2d | Poly3d | Seg3d], objects: dict[UUID, Object] +) -> dict[str, JSONObjectData] | None: + if len(annotations) == 0: + return None + + object_data = {} + + for ann_id, annotation in annotations.items(): + object_id = str(annotation.object_id) + + if object_id not in object_data: + object_data[object_id] = JSONObjectData( + object_data=JSONAnnotations( + bbox=[], + cuboid=[], + poly2d=[], + poly3d=[], + vec=[], ) + ) + + json_annotation = annotation.to_json(ann_id, objects[UUID(object_id)].type) + + if isinstance(annotation, Bbox): + object_data[object_id].object_data.bbox.append(json_annotation) # type: ignore + + elif isinstance(annotation, Cuboid): + object_data[object_id].object_data.cuboid.append(json_annotation) # type: ignore + + elif isinstance(annotation, Poly2d): + object_data[object_id].object_data.poly2d.append(json_annotation) # type: ignore + + elif isinstance(annotation, Poly3d): + object_data[object_id].object_data.poly3d.append(json_annotation) # type: ignore + + elif isinstance(annotation, Seg3d): + object_data[object_id].object_data.vec.append(json_annotation) # type: ignore + + else: + raise TypeError + + for object_id in object_data: + object_data[object_id].object_data.bbox = _empty_list_to_none( + object_data[object_id].object_data.bbox + ) + object_data[object_id].object_data.cuboid = _empty_list_to_none( + object_data[object_id].object_data.cuboid + ) + object_data[object_id].object_data.poly2d = _empty_list_to_none( + object_data[object_id].object_data.poly2d + ) + object_data[object_id].object_data.poly3d = _empty_list_to_none( + object_data[object_id].object_data.poly3d + ) + object_data[object_id].object_data.vec = _empty_list_to_none( + object_data[object_id].object_data.vec + ) - return annotations_dict + return object_data diff --git a/raillabel/format/frame_interval.py b/raillabel/format/frame_interval.py index 8780785..6b9cd68 100644 --- a/raillabel/format/frame_interval.py +++ b/raillabel/format/frame_interval.py @@ -27,28 +27,13 @@ def from_json(cls, json: JSONFrameInterval) -> FrameInterval: ) @classmethod - def fromdict(cls, data_dict: dict) -> FrameInterval: - """Generate a FrameInterval object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return FrameInterval( - start=data_dict["frame_start"], - end=data_dict["frame_end"], - ) - - @classmethod - def from_frame_uids(cls, frame_uids: list[int]) -> list[FrameInterval]: + def from_frame_ids(cls, frame_ids: list[int]) -> list[FrameInterval]: """Convert a list of frame uids into FrameIntervals. Example: ------- ```python - FrameInterval.from_frame_uids([0, 1, 2, 3, 9, 12, 13, 14]) == [ + FrameInterval.from_frame_ids([0, 1, 2, 3, 9, 12, 13, 14]) == [ FrameInterval(0, 3), FrameInterval(9, 9), FrameInterval(12, 14), @@ -56,54 +41,42 @@ def from_frame_uids(cls, frame_uids: list[int]) -> list[FrameInterval]: ``` """ - sorted_frame_uids = sorted(frame_uids) - frame_uid_intervals = cls._slice_into_intervals(sorted_frame_uids) + sorted_frame_ids = sorted(frame_ids) + frame_id_intervals = _slice_into_intervals(sorted_frame_ids) return [ - FrameInterval(start=interval[0], end=interval[-1]) for interval in frame_uid_intervals + FrameInterval(start=interval[0], end=interval[-1]) for interval in frame_id_intervals ] - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "frame_start": int(self.start), - "frame_end": int(self.end), - } + def to_json(self) -> JSONFrameInterval: + """Export this object into the RailLabel JSON format.""" + return JSONFrameInterval( + frame_start=self.start, + frame_end=self.end, + ) def __len__(self) -> int: """Return the length in frames.""" return abs(self.start - self.end) + 1 - @classmethod - def _slice_into_intervals(cls, sorted_frame_uids: list[int]) -> list[list[int]]: - if len(sorted_frame_uids) == 0: - return [] - if len(sorted_frame_uids) == 1: - return [sorted_frame_uids] +def _slice_into_intervals(sorted_frame_ids: list[int]) -> list[list[int]]: + if len(sorted_frame_ids) == 0: + return [] + + if len(sorted_frame_ids) == 1: + return [sorted_frame_ids] - intervals = [] - interval_start_i = 0 - for i, frame_uid in enumerate(sorted_frame_uids[1:]): - previous_frame_uid = sorted_frame_uids[i] + intervals = [] + interval_start_i = 0 + for i, frame_id in enumerate(sorted_frame_ids[1:]): + previous_frame_id = sorted_frame_ids[i] - if frame_uid - previous_frame_uid > 1: - intervals.append(sorted_frame_uids[interval_start_i : i + 1]) - interval_start_i = i + 1 + if frame_id - previous_frame_id > 1: + intervals.append(sorted_frame_ids[interval_start_i : i + 1]) + interval_start_i = i + 1 - intervals.append(sorted_frame_uids[interval_start_i : len(sorted_frame_uids)]) - interval_start_i = len(sorted_frame_uids) + intervals.append(sorted_frame_ids[interval_start_i : len(sorted_frame_ids)]) + interval_start_i = len(sorted_frame_ids) - return intervals + return intervals diff --git a/raillabel/format/gps_imu.py b/raillabel/format/gps_imu.py new file mode 100644 index 0000000..ad3610d --- /dev/null +++ b/raillabel/format/gps_imu.py @@ -0,0 +1,38 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +from ._sensor_without_intrinsics import _extrinsics_from_json, _SensorWithoutIntrinsics + + +class GpsImu(_SensorWithoutIntrinsics): + """A gps sensor with inertial measurement unit.""" + + @classmethod + def from_json( + cls, json_stream: JSONStreamOther, json_coordinate_system: JSONCoordinateSystem + ) -> GpsImu: + """Construct an instant of this class from RailLabel JSON data.""" + return GpsImu( + extrinsics=_extrinsics_from_json(json_coordinate_system.pose_wrt_parent), + uri=json_stream.uri, + description=json_stream.description, + ) + + def to_json(self) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + """Export this object into the RailLabel JSON format.""" + return ( + JSONStreamOther( + type="gps_imu", + uri=self.uri, + description=self.description, + ), + JSONCoordinateSystem( + parent="base", + type="sensor", + pose_wrt_parent=self.extrinsics.to_json() if self.extrinsics is not None else None, + ), + ) diff --git a/raillabel/format/intrinsics_pinhole.py b/raillabel/format/intrinsics_pinhole.py index 6979371..d0b8165 100644 --- a/raillabel/format/intrinsics_pinhole.py +++ b/raillabel/format/intrinsics_pinhole.py @@ -40,45 +40,11 @@ def from_json(cls, json: JSONIntrinsicsPinhole) -> IntrinsicsPinhole: height_px=json.height_px, ) - @classmethod - def fromdict(cls, data_dict: dict) -> IntrinsicsPinhole: - """Generate a IntrinsicsPinhole object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - Returns - ------- - raillabel.format.IntrinsicsPinhole - Converted IntrinsicsPinhole object. - - """ - return IntrinsicsPinhole( - camera_matrix=tuple(data_dict["camera_matrix"]), - distortion=tuple(data_dict["distortion_coeffs"]), - width_px=data_dict["width_px"], - height_px=data_dict["height_px"], + def to_json(self) -> JSONIntrinsicsPinhole: + """Export this object into the RailLabel JSON format.""" + return JSONIntrinsicsPinhole( + camera_matrix=self.camera_matrix, + distortion_coeffs=self.distortion, + width_px=self.width_px, + height_px=self.height_px, ) - - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "camera_matrix": list(self.camera_matrix), - "distortion_coeffs": list(self.distortion), - "width_px": int(self.width_px), - "height_px": int(self.height_px), - } diff --git a/raillabel/format/intrinsics_radar.py b/raillabel/format/intrinsics_radar.py index 355bfb5..579d6ae 100644 --- a/raillabel/format/intrinsics_radar.py +++ b/raillabel/format/intrinsics_radar.py @@ -31,43 +31,10 @@ def from_json(cls, json: JSONIntrinsicsRadar) -> IntrinsicsRadar: height_px=json.height_px, ) - @classmethod - def fromdict(cls, data_dict: dict) -> IntrinsicsRadar: - """Generate a IntrinsicsRadar object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - Returns - ------- - raillabel.format.IntrinsicsRadar - Converted IntrinsicsRadar object. - - """ - return IntrinsicsRadar( - resolution_px_per_m=data_dict["resolution_px_per_m"], - width_px=data_dict["width_px"], - height_px=data_dict["height_px"], + def to_json(self) -> JSONIntrinsicsRadar: + """Export this object into the RailLabel JSON format.""" + return JSONIntrinsicsRadar( + resolution_px_per_m=self.resolution_px_per_m, + width_px=self.width_px, + height_px=self.height_px, ) - - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "resolution_px_per_m": float(self.resolution_px_per_m), - "width_px": int(self.width_px), - "height_px": int(self.height_px), - } diff --git a/raillabel/format/lidar.py b/raillabel/format/lidar.py new file mode 100644 index 0000000..e69e26a --- /dev/null +++ b/raillabel/format/lidar.py @@ -0,0 +1,38 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +from ._sensor_without_intrinsics import _extrinsics_from_json, _SensorWithoutIntrinsics + + +class Lidar(_SensorWithoutIntrinsics): + """A lidar sensor.""" + + @classmethod + def from_json( + cls, json_stream: JSONStreamOther, json_coordinate_system: JSONCoordinateSystem + ) -> Lidar: + """Construct an instant of this class from RailLabel JSON data.""" + return Lidar( + extrinsics=_extrinsics_from_json(json_coordinate_system.pose_wrt_parent), + uri=json_stream.uri, + description=json_stream.description, + ) + + def to_json(self) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + """Export this object into the RailLabel JSON format.""" + return ( + JSONStreamOther( + type="lidar", + uri=self.uri, + description=self.description, + ), + JSONCoordinateSystem( + parent="base", + type="sensor", + pose_wrt_parent=self.extrinsics.to_json() if self.extrinsics is not None else None, + ), + ) diff --git a/raillabel/format/metadata.py b/raillabel/format/metadata.py index b863b97..b8ae018 100644 --- a/raillabel/format/metadata.py +++ b/raillabel/format/metadata.py @@ -4,122 +4,67 @@ from __future__ import annotations from dataclasses import dataclass -from importlib import metadata as importlib_metadata + +from raillabel.json_format import JSONMetadata @dataclass class Metadata: - """Container for metadata information about the scene itself. - - As the OpenLABEL metadata object accepts additional properties, so does this class. Any - properties present in the JSON will be added to the Metadata() object when read through - Metadata.fromdict(). Conversely, all attributes from the Metadata() object will be stored - into the JSON when using Metadata.asdict(). You can therefore just add attributes to the - Python object and have them stored. - Example: - m = Metadata.fromdict( - { - "schema_version": "1.0.0", - "some_additional_property": "Some Value" - } - ) - m.another_additional_property = "Another Value" - m.asdict() - -> { - "schema_version": "1.0.0", - "some_additional_property": "Some Value", - "another_additional_property": "Another Value" - } - - Parameters - ---------- - schema_version: str - Version number of the OpenLABEL schema this annotation object follows. - annotator: str, optional - Name or description of the annotator that created the annotations. Default is None. - comment: str, optional - Additional information or description about the annotation content. Default is None. - exporter_version: str, optional - Version of the raillabel-devkit, that last exported the scene. Default is None. - file_version: str, optional - Version number of the raillabel annotation content. Default is None. - name: str, optional - Name of the raillabel annotation content. Default is None. - subschema_version: str, optional - Version number of the RailLabel schema this annotation object follows. Default is None. - tagged_file: str, optional - Directory with the exported data_dict (e.g. images, point clouds). Default is None. - - """ + """Container for metadata information about the scene itself.""" schema_version: str + "Version number of the OpenLABEL schema this annotation object follows." + annotator: str | None = None + "Name or description of the annotator that created the annotations." + comment: str | None = None + "Additional information or description about the annotation content." + exporter_version: str | None = None + "Version of the raillabel-devkit, that last exported the scene." + file_version: str | None = None + "Version number of the raillabel annotation content." + name: str | None = None + "Name of the raillabel annotation content." + subschema_version: str | None = None + "Version number of the RailLabel schema this annotation object follows." + tagged_file: str | None = None + "Directory with the exported data_dict (e.g. images, point clouds)." @classmethod - def fromdict(cls, data_dict: dict, subschema_version: str | None = None) -> Metadata: - """Generate a Metadata object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. Additional (non-defined) - arguments can be set and will be added as properties to Metadata. - subschema_version: str, optional - Version of the RailLabel subschema - - Returns - ------- - metadata: Metadata - Converted metadata. - - """ + def from_json(cls, json: JSONMetadata) -> Metadata: + """Construct an instant of this class from RailLabel JSON data.""" metadata = Metadata( - schema_version=data_dict["schema_version"], - subschema_version=subschema_version, - exporter_version=cls._collect_exporter_version(), + schema_version=json.schema_version, + name=json.name, + subschema_version=json.subschema_version, + exporter_version=json.exporter_version, + file_version=json.file_version, + tagged_file=json.tagged_file, + annotator=json.annotator, + comment=json.comment, ) - return cls._set_additional_attributes(metadata, data_dict) - - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - """ - return self._remove_empty_fields(vars(self)) - - @classmethod - def _collect_exporter_version(cls) -> str | None: - try: - exporter_version = importlib_metadata.version("raillabel") - except importlib_metadata.PackageNotFoundError: - return None - - version_number_length = len(exporter_version) - len(exporter_version.split(".")[-1]) - return exporter_version[: version_number_length - 1] - - @classmethod - def _set_additional_attributes(cls, metadata: Metadata, data_dict: dict) -> Metadata: - preset_keys = ["schema_version", "subschema_version", "exporter_version"] - - for key, value in data_dict.items(): - if key in preset_keys: - continue - - setattr(metadata, key, value) + if json.model_extra is not None: + for extra_field, extra_value in json.model_extra.items(): + setattr(metadata, extra_field, extra_value) return metadata - def _remove_empty_fields(self, dict_repr: dict) -> dict: - """Remove empty fields from a dictionary.""" - return {k: v for k, v in dict_repr.items() if v is not None} + def to_json(self) -> JSONMetadata: + """Export this object into the RailLabel JSON format.""" + return JSONMetadata( + schema_version=self.schema_version, + name=self.name, + subschema_version=self.subschema_version, + exporter_version=self.exporter_version, + file_version=self.file_version, + tagged_file=self.tagged_file, + annotator=self.annotator, + comment=self.comment, + ) diff --git a/raillabel/format/num.py b/raillabel/format/num.py index 94c538f..e77d1c0 100644 --- a/raillabel/format/num.py +++ b/raillabel/format/num.py @@ -4,80 +4,42 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from .sensor import Sensor +from raillabel.json_format import JSONNum @dataclass class Num: - """A number. + """A number.""" - Parameters - ---------- - uid: str - This a string representing the unique universal identifier of the annotation. name: str - Human readable name describing the annotation. - val: int or float - This is the value of the number object. - attributes: dict, optional - Attributes of the annotation. Dict keys are the uid str of the attribute, values are the - attribute values. Default is {}. - sensor: raillabel.format.Sensor - A reference to the sensor, this value is represented in. Default is None. + "Human readable name describing the annotation." - """ + val: float + "This is the value of the number object." - uid: str - name: str - val: int | float - sensor: Sensor - - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict) -> Num: - """Generate a Num object from a dict. + id: UUID | None = None + "The unique identifyer of the Num." - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. + sensor_id: str | None = None + "A reference to the sensor, this value is represented in." - Returns - ------- - annotation: Num - Converted annotation. - - """ + @classmethod + def from_json(cls, json: JSONNum) -> Num: + """Construct an instant of this class from RailLabel JSON data.""" return Num( - uid=str(data_dict["uid"]), - name=str(data_dict["name"]), - val=data_dict["val"], - sensor=cls._coordinate_system_fromdict(data_dict, sensors), + name=json.name, + val=json.val, + id=json.uid, + sensor_id=json.coordinate_system, ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "uid": str(self.uid), - "name": str(self.name), - "val": self.val, - "coordinate_system": str(self.sensor.uid), - } - - @classmethod - def _coordinate_system_fromdict(cls, data_dict: dict, sensors: dict) -> Sensor: - return sensors[data_dict["coordinate_system"]] + def to_json(self) -> JSONNum: + """Export this object into the RailLabel JSON format.""" + return JSONNum( + name=self.name, + val=self.val, + coordinate_system=self.sensor_id, + uid=self.id, + ) diff --git a/raillabel/format/object.py b/raillabel/format/object.py index 9301ce0..6a3eaa3 100644 --- a/raillabel/format/object.py +++ b/raillabel/format/object.py @@ -3,222 +3,102 @@ from __future__ import annotations -import typing as t from dataclasses import dataclass +from typing import TYPE_CHECKING +from uuid import UUID -from .element_data_pointer import AttributeType, ElementDataPointer +from raillabel.json_format import JSONElementDataPointer, JSONFrameInterval, JSONObject + +from ._attributes import _attributes_to_json from .frame_interval import FrameInterval -if t.TYPE_CHECKING: +if TYPE_CHECKING: from .frame import Frame @dataclass class Object: - """Physical, unique object in the data, that can be tracked via its UID. + """Physical, unique object in the data, that can be tracked via its UID.""" - Parameters - ---------- - uid: str - This a string representing the unique universal identifier for the object. name: str - Name of the object. It is a friendly name and not used for indexing. Commonly the class - name is used followed by an underscore and an integer (i.e. person_0032). - type: str - The type of an object defines the class the object corresponds to. - - """ + """Name of the object. It is a friendly name and not used for indexing. Commonly the class name + is used followed by an underscore and an integer (i.e. person_0032).""" - uid: str - name: str type: str - - # --- Public Methods -------------- + "The type of an object defines the class the object corresponds to (like 'person')." @classmethod - def fromdict(cls, data_dict: dict, object_uid: str) -> Object: - """Generate a Object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - object_uid: str - Unique identifier of the object. - - Returns - ------- - object: raillabel.format.Object - Converted object. - - """ - return Object(uid=object_uid, type=data_dict["type"], name=data_dict["name"]) - - def asdict(self, frames: dict[int, Frame] | None = None) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - frames: dict, optional - The dictionary of frames stored under Scene.frames used for the frame intervals and - object data pointers. If None, these are not provided. Default is None. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - if frames is None: - return {"name": str(self.name), "type": str(self.type)} - - return { - "name": str(self.name), - "type": str(self.type), - "frame_intervals": self._frame_intervals_asdict(self.frame_intervals(frames)), - "object_data_pointers": self._object_data_pointers_asdict( - self.object_data_pointers(frames) - ), - } - - def frame_intervals(self, frames: dict[int, Frame]) -> list[FrameInterval]: - """Return frame intervals in which this object is present. - - Parameters - ---------- - frames: dict[int, raillabel.format.Frame] - The dictionary of frames stored under Scene.frames. - - Returns - ------- - list[FrameInterval] - List of the FrameIntervals, where this object is contained. - - """ - frame_uids_containing_object = [ - frame_uid for frame_uid, frame in frames.items() if self._is_object_in_frame(frame) - ] - - return FrameInterval.from_frame_uids(frame_uids_containing_object) - - def object_data_pointers(self, frames: dict[int, Frame]) -> dict[str, ElementDataPointer]: - """Create object data pointers used in WebLABEL visualization. - - Parameters - ---------- - frames: dict[int, raillabel.format.Frame] - The dictionary of frames stored under Scene.frames. - - Returns - ------- - dict[str, ElementDataPointer] - ObjectDataPointers dict as required by WebLABEL. Keys are the ObjectDataPointer uids. - - """ - pointer_ids_per_frame = self._collect_pointer_ids_per_frame(frames) - frame_uids_per_pointer_id = self._reverse_frame_pointer_ids(pointer_ids_per_frame) - frame_intervals_per_pointer_id = self._convert_to_intervals(frame_uids_per_pointer_id) - - attributes_per_pointer_id = self._collect_attributes_per_pointer_id(frames) - attribute_pointers_per_pointer_id = self._convert_to_attribute_pointers( - attributes_per_pointer_id + def from_json(cls, json: JSONObject) -> Object: + """Construct an instant of this class from RailLabel JSON data.""" + return Object( + name=json.name, + type=json.type, ) - return self._create_object_data_pointers( - frame_intervals_per_pointer_id, attribute_pointers_per_pointer_id + def to_json(self, object_id: UUID, frames: dict[int, Frame]) -> JSONObject: + """Export this object into the RailLabel JSON format.""" + return JSONObject( + name=self.name, + type=self.type, + frame_intervals=_frame_intervals_to_json(object_id, frames), + object_data_pointers=_object_data_pointers_to_json(object_id, self.type, frames), + ) + + +def _frame_intervals_to_json(object_id: UUID, frames: dict[int, Frame]) -> list[JSONFrameInterval]: + frames_with_this_object = set() + + for frame_id, frame in frames.items(): + for annotation in frame.annotations.values(): + if annotation.object_id == object_id: + frames_with_this_object.add(frame_id) + continue + + return [fi.to_json() for fi in FrameInterval.from_frame_ids(list(frames_with_this_object))] + + +def _object_data_pointers_to_json( + object_id: UUID, object_type: str, frames: dict[int, Frame] +) -> dict[str, JSONElementDataPointer]: + pointers_raw = {} + + for frame_id, frame in frames.items(): + for annotation in [ann for ann in frame.annotations.values() if ann.object_id == object_id]: + annotation_name = annotation.name(object_type) + if annotation_name not in pointers_raw: + pointers_raw[annotation_name] = { + "frame_intervals": set(), + "type": annotation_name.split("__")[1], + "attribute_pointers": {}, + } + + pointers_raw[annotation_name]["frame_intervals"].add(frame_id) # type: ignore + json_attributes = _attributes_to_json(annotation.attributes) + + if json_attributes is None: + continue + + for attribute in json_attributes.boolean: # type: ignore + pointers_raw[annotation_name]["attribute_pointers"][attribute.name] = "boolean" # type: ignore + + for attribute in json_attributes.num: # type: ignore + pointers_raw[annotation_name]["attribute_pointers"][attribute.name] = "num" # type: ignore + + for attribute in json_attributes.text: # type: ignore + pointers_raw[annotation_name]["attribute_pointers"][attribute.name] = "text" # type: ignore + + for attribute in json_attributes.vec: # type: ignore + pointers_raw[annotation_name]["attribute_pointers"][attribute.name] = "vec" # type: ignore + + object_data_pointers = {} + for annotation_name, object_data_pointer in pointers_raw.items(): + object_data_pointers[annotation_name] = JSONElementDataPointer( + type=object_data_pointer["type"], + frame_intervals=[ + fi.to_json() + for fi in FrameInterval.from_frame_ids(list(object_data_pointer["frame_intervals"])) # type: ignore + ], + attribute_pointers=object_data_pointer["attribute_pointers"], ) - # --- Private Methods ------------- - - def _frame_intervals_asdict( - self, frame_intervals: list[FrameInterval] - ) -> list[dict[str, t.Any]]: - return [fi.asdict() for fi in frame_intervals] - - def _object_data_pointers_asdict( - self, object_data_pointers: dict[str, ElementDataPointer] - ) -> dict: - return {pointer_id: pointer.asdict() for pointer_id, pointer in object_data_pointers.items()} - - def _is_object_in_frame(self, frame: Frame) -> bool: - return self.uid in frame.object_data - - def _filtered_annotations(self, frame: Frame) -> list[t.Any]: - return [ann for ann in frame.annotations.values() if ann.object.uid == self.uid] - - def _collect_pointer_ids_per_frame(self, frames: dict[int, Frame]) -> dict[int, set[str]]: - pointer_ids_per_frame: dict[int, set[str]] = {} - for frame_uid, frame in frames.items(): - pointer_ids_per_frame[frame_uid] = set() - - for annotation in self._filtered_annotations(frame): - pointer_ids_per_frame[frame_uid].add(annotation.name) # type: ignore - - return pointer_ids_per_frame - - def _reverse_frame_pointer_ids( - self, pointer_ids_per_frame: dict[int, set[str]] - ) -> dict[str, set[int]]: - frame_uids_per_pointer_id: dict[str, set[int]] = {} - for frame_uid, pointer_ids in pointer_ids_per_frame.items(): - for pointer_id in pointer_ids: - if pointer_id not in frame_uids_per_pointer_id: - frame_uids_per_pointer_id[pointer_id] = set() - - frame_uids_per_pointer_id[pointer_id].add(frame_uid) - - return frame_uids_per_pointer_id - - def _convert_to_intervals( - self, frame_uids_per_pointer_id: dict[str, set[int]] - ) -> dict[str, list[FrameInterval]]: - frame_intervals = {} - for pointer_id, frame_uids in frame_uids_per_pointer_id.items(): - frame_intervals[pointer_id] = FrameInterval.from_frame_uids(list(frame_uids)) - - return frame_intervals - - def _collect_attributes_per_pointer_id( - self, frames: dict[int, Frame] - ) -> dict[str, dict[str, t.Any]]: - attributes_per_pointer_id: dict[str, dict[str, t.Any]] = {} - for frame in frames.values(): - for annotation in self._filtered_annotations(frame): - if annotation.name not in attributes_per_pointer_id: # type: ignore - attributes_per_pointer_id[annotation.name] = {} # type: ignore - - attributes_per_pointer_id[annotation.name].update(annotation.attributes) # type: ignore - - return attributes_per_pointer_id - - def _convert_to_attribute_pointers( - self, attributes_per_pointer_id: dict[str, dict[str, t.Any]] - ) -> dict[str, dict[str, AttributeType]]: - for attributes in attributes_per_pointer_id.values(): - for attribute_name, attribute_value in attributes.items(): - attributes[attribute_name] = AttributeType.from_value(type(attribute_value)) - - return attributes_per_pointer_id - - def _create_object_data_pointers( - self, - frame_intervals_per_pointer_id: dict[str, list[FrameInterval]], - attribute_pointers_per_pointer_id: dict[str, dict[str, AttributeType]], - ) -> dict[str, ElementDataPointer]: - object_data_pointers = {} - for pointer_id in frame_intervals_per_pointer_id: - object_data_pointers[pointer_id] = ElementDataPointer( - uid=pointer_id, - frame_intervals=frame_intervals_per_pointer_id[pointer_id], - attribute_pointers=attribute_pointers_per_pointer_id[pointer_id], - ) - - return object_data_pointers - - # --- Special Methods ------------- - - def __hash__(self) -> int: - """Return hash.""" - return self.uid.__hash__() + return object_data_pointers diff --git a/raillabel/format/other_sensor.py b/raillabel/format/other_sensor.py new file mode 100644 index 0000000..5a1b353 --- /dev/null +++ b/raillabel/format/other_sensor.py @@ -0,0 +1,38 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +from ._sensor_without_intrinsics import _extrinsics_from_json, _SensorWithoutIntrinsics + + +class OtherSensor(_SensorWithoutIntrinsics): + """A sensor that is not represented by the available options.""" + + @classmethod + def from_json( + cls, json_stream: JSONStreamOther, json_coordinate_system: JSONCoordinateSystem + ) -> OtherSensor: + """Construct an instant of this class from RailLabel JSON data.""" + return OtherSensor( + extrinsics=_extrinsics_from_json(json_coordinate_system.pose_wrt_parent), + uri=json_stream.uri, + description=json_stream.description, + ) + + def to_json(self) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + """Export this object into the RailLabel JSON format.""" + return ( + JSONStreamOther( + type="other", + uri=self.uri, + description=self.description, + ), + JSONCoordinateSystem( + parent="base", + type="sensor", + pose_wrt_parent=self.extrinsics.to_json() if self.extrinsics is not None else None, + ), + ) diff --git a/raillabel/format/point2d.py b/raillabel/format/point2d.py index 07566b0..d50b7bd 100644 --- a/raillabel/format/point2d.py +++ b/raillabel/format/point2d.py @@ -10,32 +10,17 @@ class Point2d: """A 2d point in an image.""" - x: int | float + x: float "The x-coordinate of the point in the image in pixels from the left." - y: int | float + y: float "The y-coordinate of the point in the image in pixels from the top." @classmethod - def from_json(cls, json: tuple[int | float, int | float]) -> Point2d: + def from_json(cls, json: tuple[float, float]) -> Point2d: """Construct an instant of this class from RailLabel JSON data.""" return Point2d(x=json[0], y=json[1]) - @classmethod - def fromdict(cls, data_dict: dict) -> Point2d: - """Generate a Point2d object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Point2d( - x=data_dict[0], - y=data_dict[1], - ) - - def asdict(self) -> list[float]: - """Export self as a dict compatible with the OpenLABEL schema.""" - return [float(self.x), float(self.y)] + def to_json(self) -> tuple[float, float]: + """Export this object into the RailLabel JSON format.""" + return (self.x, self.y) diff --git a/raillabel/format/point3d.py b/raillabel/format/point3d.py index f916a6f..efca84a 100644 --- a/raillabel/format/point3d.py +++ b/raillabel/format/point3d.py @@ -24,22 +24,6 @@ def from_json(cls, json: tuple[float, float, float]) -> Point3d: """Construct an instant of this class from RailLabel JSON data.""" return Point3d(x=json[0], y=json[1], z=json[2]) - @classmethod - def fromdict(cls, data_dict: dict) -> Point3d: - """Generate a Point3d object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Point3d( - x=data_dict[0], - y=data_dict[1], - z=data_dict[2], - ) - - def asdict(self) -> list[float]: - """Export self as a dict compatible with the OpenLABEL schema.""" - return [float(self.x), float(self.y), float(self.z)] + def to_json(self) -> tuple[float, float, float]: + """Export this object into the RailLabel JSON format.""" + return (self.x, self.y, self.z) diff --git a/raillabel/format/poly2d.py b/raillabel/format/poly2d.py index 1952714..ed20388 100644 --- a/raillabel/format/poly2d.py +++ b/raillabel/format/poly2d.py @@ -4,111 +4,60 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from ._object_annotation import _ObjectAnnotation -from .object import Object +from raillabel.json_format import JSONPoly2d + +from ._attributes import _attributes_from_json, _attributes_to_json +from ._util import _flatten_list from .point2d import Point2d @dataclass -class Poly2d(_ObjectAnnotation): - """Sequence of 2D points. Can either be a polygon or polyline. - - Parameters - ---------- - uid: str - This a string representing the unique universal identifier for the annotation. - points: list of raillabel.format.Point2d - List of the 2d points that make up the polyline. - closed: bool - This parameter states, whether the polyline represents a closed shape (a polygon) or an - open line. - mode: str, optional - Mode of the polyline list of values: "MODE_POLY2D_ABSOLUTE" determines that the poly2d list - contains the sequence of (x, y) values of all points of the polyline. "MODE_POLY2D_RELATIVE" - specifies that only the first point of the sequence is defined with its (x, y) values, while - all the rest are defined relative to it. "MODE_POLY2D_SRF6DCC" specifies that SRF6DCC chain - code method is used. "MODE_POLY2D_RS6FCC" specifies that the RS6FCC method is used. Default - is 'MODE_POLY2D_ABSOLUTE'. - object: raillabel.format.Object - A reference to the object, this annotation belongs to. - sensor: raillabel.format.Sensor - A reference to the sensor, this annotation is labeled in. Default is None. - attributes: dict, optional - Attributes of the annotation. Dict keys are the name str of the attribute, values are the - attribute values. Default is {}. - - Properties (read-only) - ---------------------- - name: str - Name of the annotation used by the VCD player for indexing in the object data pointers. - - """ +class Poly2d: + """Sequence of 2D points. Can either be a polygon or polyline.""" points: list[Point2d] - closed: bool - mode: str = "MODE_POLY2D_ABSOLUTE" + "List of the 2d points that make up the polyline." - OPENLABEL_ID = "poly2d" + closed: bool + "If True, this object represents a polygon and if False, it represents a polyline." - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict, object: Object) -> Poly2d: - """Generate a Poly2d object from a dict. + object_id: UUID + "The unique identifyer of the real-life object, this annotation belongs to." - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. - object: raillabel.format.Object - Object this annotation belongs to. + sensor_id: str + "The unique identifyer of the sensor this annotation is labeled in." - Returns - ------- - annotation: Poly2d - Converted annotation. + attributes: dict[str, float | bool | str | list] + "Additional information associated with the annotation." - """ + @classmethod + def from_json(cls, json: JSONPoly2d, object_id: UUID) -> Poly2d: + """Construct an instant of this class from RailLabel JSON data.""" return Poly2d( - uid=str(data_dict["uid"]), - closed=data_dict["closed"], - mode=data_dict["mode"], - points=cls._points_fromdict(data_dict), - object=object, - sensor=cls._coordinate_system_fromdict(data_dict, sensors), - attributes=cls._attributes_fromdict(data_dict), + points=[ + Point2d(x=float(json.val[i]), y=float(json.val[i + 1])) + for i in range(0, len(json.val), 2) + ], + closed=json.closed, + object_id=object_id, + sensor_id=json.coordinate_system, + attributes=_attributes_from_json(json.attributes), ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr = self._annotation_required_fields_asdict() - - dict_repr["closed"] = bool(self.closed) - dict_repr["val"] = [] - dict_repr["mode"] = self.mode - for point in self.points: - dict_repr["val"].extend(point.asdict()) - - dict_repr.update(self._annotation_optional_fields_asdict()) - - return dict_repr + def to_json(self, uid: UUID, object_type: str) -> JSONPoly2d: + """Export this object into the RailLabel JSON format.""" + return JSONPoly2d( + name=self.name(object_type), + val=_flatten_list([point.to_json() for point in self.points]), + closed=self.closed, + mode="MODE_POLY2D_ABSOLUTE", + coordinate_system=self.sensor_id, + uid=uid, + attributes=_attributes_to_json(self.attributes), + ) - @classmethod - def _points_fromdict(cls, data_dict: dict) -> list[Point2d]: - return [ - Point2d(x=data_dict["val"][i], y=data_dict["val"][i + 1]) - for i in range(0, len(data_dict["val"]), 2) - ] + def name(self, object_type: str) -> str: + """Return the name of the annotation used for indexing in the object data pointers.""" + return f"{self.sensor_id}__poly2d__{object_type}" diff --git a/raillabel/format/poly3d.py b/raillabel/format/poly3d.py index d993adc..b49ff4e 100644 --- a/raillabel/format/poly3d.py +++ b/raillabel/format/poly3d.py @@ -4,101 +4,59 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from ._object_annotation import _ObjectAnnotation -from .object import Object +from raillabel.json_format import JSONPoly3d + +from ._attributes import _attributes_from_json, _attributes_to_json +from ._util import _flatten_list from .point3d import Point3d @dataclass -class Poly3d(_ObjectAnnotation): - """Sequence of 3D points. Can either be a polygon or polyline. - - Parameters - ---------- - uid: str - This a string representing the unique universal identifier for the annotation. - points: list of raillabel.format.Point3d - List of the 3d points that make up the polyline. - closed: bool - This parameter states, whether the polyline represents a closed shape (a polygon) or an - open line. - object: raillabel.format.Object - A reference to the object, this annotation belongs to. - sensor: raillabel.format.Sensor - A reference to the sensor, this annotation is labeled in. Default is None. - attributes: dict, optional - Attributes of the annotation. Dict keys are the name str of the attribute, values are the - attribute values. Default is {}. - - Properties (read-only) - ---------------------- - name: str - Name of the annotation used by the VCD player for indexing in the object data pointers. - - """ +class Poly3d: + """Sequence of 3D points. Can either be a polygon or polyline.""" points: list[Point3d] - closed: bool + "List of the 3d points that make up the polyline." - OPENLABEL_ID = "poly3d" + closed: bool + "If True, this object represents a polygon and if False, it represents a polyline." - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict, object: Object) -> Poly3d: - """Generate a Poly3d object from a dict. + object_id: UUID + "The unique identifyer of the real-life object, this annotation belongs to." - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. - object: raillabel.format.Object - Object this annotation belongs to. + sensor_id: str + "The unique identifyer of the sensor this annotation is labeled in." - Returns - ------- - annotation: Poly3d - Converted annotation. + attributes: dict[str, float | bool | str | list] + "Additional information associated with the annotation." - """ + @classmethod + def from_json(cls, json: JSONPoly3d, object_id: UUID) -> Poly3d: + """Construct an instant of this class from RailLabel JSON data.""" return Poly3d( - uid=str(data_dict["uid"]), - closed=data_dict["closed"], - points=cls._points_fromdict(data_dict), - object=object, - sensor=cls._coordinate_system_fromdict(data_dict, sensors), - attributes=cls._attributes_fromdict(data_dict), + points=[ + Point3d(x=float(json.val[i]), y=float(json.val[i + 1]), z=float(json.val[i + 2])) + for i in range(0, len(json.val), 3) + ], + closed=json.closed, + object_id=object_id, + sensor_id=json.coordinate_system, + attributes=_attributes_from_json(json.attributes), ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr = self._annotation_required_fields_asdict() - - dict_repr["closed"] = bool(self.closed) - dict_repr["val"] = [] - for point in self.points: - dict_repr["val"].extend(point.asdict()) - - dict_repr.update(self._annotation_optional_fields_asdict()) - - return dict_repr + def to_json(self, uid: UUID, object_type: str) -> JSONPoly3d: + """Export this object into the RailLabel JSON format.""" + return JSONPoly3d( + name=self.name(object_type), + val=_flatten_list([point.to_json() for point in self.points]), + closed=self.closed, + coordinate_system=self.sensor_id, + uid=uid, + attributes=_attributes_to_json(self.attributes), + ) - @classmethod - def _points_fromdict(cls, data_dict: dict) -> list[Point3d]: - return [ - Point3d(x=data_dict["val"][i], y=data_dict["val"][i + 1], z=data_dict["val"][i + 2]) - for i in range(0, len(data_dict["val"]), 3) - ] + def name(self, object_type: str) -> str: + """Return the name of the annotation used for indexing in the object data pointers.""" + return f"{self.sensor_id}__poly3d__{object_type}" diff --git a/raillabel/format/quaternion.py b/raillabel/format/quaternion.py index 366747a..a6a4df5 100644 --- a/raillabel/format/quaternion.py +++ b/raillabel/format/quaternion.py @@ -27,23 +27,6 @@ def from_json(cls, json: tuple[float, float, float, float]) -> Quaternion: """Construct an instant of this class from RailLabel JSON data.""" return Quaternion(x=json[0], y=json[1], z=json[2], w=json[3]) - @classmethod - def fromdict(cls, data_dict: dict) -> Quaternion: - """Generate a Quaternion object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Quaternion( - x=data_dict[0], - y=data_dict[1], - z=data_dict[2], - w=data_dict[3], - ) - - def asdict(self) -> list[float]: - """Export self as a dict compatible with the OpenLABEL schema.""" - return [float(self.x), float(self.y), float(self.z), float(self.w)] + def to_json(self) -> tuple[float, float, float, float]: + """Export this object into the RailLabel JSON format.""" + return (self.x, self.y, self.z, self.w) diff --git a/raillabel/format/radar.py b/raillabel/format/radar.py new file mode 100644 index 0000000..702aa32 --- /dev/null +++ b/raillabel/format/radar.py @@ -0,0 +1,69 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from raillabel.json_format import ( + JSONCoordinateSystem, + JSONStreamRadar, + JSONStreamRadarProperties, + JSONTransformData, +) + +from .intrinsics_radar import IntrinsicsRadar +from .transform import Transform + + +@dataclass +class Radar: + """A radar sensor.""" + + intrinsics: IntrinsicsRadar + "The intrinsic calibration of the sensor." + + extrinsics: Transform | None = None + "External calibration of the sensor defined by the 3D transform to the coordinate system origin." + + uri: str | None = None + "Name of the subdirectory containing the sensor files." + + description: str | None = None + "Additional information about the sensor." + + @classmethod + def from_json( + cls, json_stream: JSONStreamRadar, json_coordinate_system: JSONCoordinateSystem + ) -> Radar: + """Construct an instant of this class from RailLabel JSON data.""" + return Radar( + intrinsics=IntrinsicsRadar.from_json(json_stream.stream_properties.intrinsics_radar), + extrinsics=_extrinsics_from_json(json_coordinate_system.pose_wrt_parent), + uri=json_stream.uri, + description=json_stream.description, + ) + + def to_json(self) -> tuple[JSONStreamRadar, JSONCoordinateSystem]: + """Export this object into the RailLabel JSON format.""" + return ( + JSONStreamRadar( + type="radar", + stream_properties=JSONStreamRadarProperties( + intrinsics_radar=self.intrinsics.to_json() + ), + uri=self.uri, + description=self.description, + ), + JSONCoordinateSystem( + parent="base", + type="sensor", + pose_wrt_parent=self.extrinsics.to_json() if self.extrinsics is not None else None, + ), + ) + + +def _extrinsics_from_json(json_transform: JSONTransformData | None) -> Transform | None: + if json_transform is None: + return None + return Transform.from_json(json_transform) diff --git a/raillabel/format/scene.py b/raillabel/format/scene.py index abe5bf4..9220794 100644 --- a/raillabel/format/scene.py +++ b/raillabel/format/scene.py @@ -4,220 +4,132 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any - +from uuid import UUID + +from raillabel.json_format import ( + JSONCoordinateSystem, + JSONFrame, + JSONObject, + JSONScene, + JSONSceneContent, + JSONStreamCamera, + JSONStreamOther, + JSONStreamRadar, +) + +from .camera import Camera from .frame import Frame from .frame_interval import FrameInterval +from .gps_imu import GpsImu +from .lidar import Lidar from .metadata import Metadata from .object import Object -from .sensor import Sensor +from .other_sensor import OtherSensor +from .radar import Radar @dataclass class Scene: - """The root RailLabel class, which contains all data. - - Parameters - ---------- - metadata: raillabel.format.Metadata - This object contains information, that is, metadata, about the annotation file itself. - sensors: dict of raillabel.format.Sensor, optional - Dictionary of raillabel.format.Sensors. Dictionary keys are the sensor uids. Default is {}. - objects: dict of raillabel.format.Object, optional - Dictionary of raillabel.format.Objects. Dictionary keys are the object uids. Default is {}. - frames: dict of raillabel.format.Frame, optional - Dict of frames in the scene. Dictionary keys are the frame uids. Default is {}. - - Properties (read-only) - ---------------------- - frame_intervals: list[FrameIntervals] - List of frame intervals describing the frames present in this scene. - - """ + """The root RailLabel class, which contains all data.""" metadata: Metadata - sensors: dict[str, Sensor] = field(default_factory=dict) - objects: dict[str, Object] = field(default_factory=dict) - frames: dict[int, Frame] = field(default_factory=dict) + "Container of information about the annotation file itself." - @property - def frame_intervals(self) -> list[FrameInterval]: - """Return frame intervals of the present frames.""" - return FrameInterval.from_frame_uids(list(self.frames.keys())) + sensors: dict[str, Camera | Lidar | Radar | GpsImu | OtherSensor] = field(default_factory=dict) + "The sensors used in this scene. Keys are sensor names." - # === Public Methods ========================================================================== + objects: dict[UUID, Object] = field(default_factory=dict) + "Unique objects (like a specific person) in this scene. Keys are object uuids" - @classmethod - def fromdict(cls, data_dict: dict, subschema_version: str | None = None) -> Scene: - """Generate a Scene object from a RailLABEL-dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - subschema_version: str, optional - Version of the RailLabel subschema. - - Returns - ------- - raillabel.format.Scene - Converted Scene object. - - Raises - ------ - raillabel.exceptions.MissingCoordinateSystemError - if a stream has no corresponding coordinate system. - raillabel.exceptions.MissingStreamError - if a coordinate system has no corresponding stream. - raillabel.exceptions.UnsupportedParentError - if a coordinate system has no corresponding stream. - - """ - data_dict = cls._prepare_data(data_dict) - - sensors = cls._sensors_fromdict(data_dict["streams"], data_dict["coordinate_systems"]) - objects = cls._objects_fromdict(data_dict["objects"]) + frames: dict[int, Frame] = field(default_factory=dict) + "A container of dynamic, timewise, information. Keys are the frame integer number." + @classmethod + def from_json(cls, json: JSONScene) -> Scene: + """Construct a scene from a json object.""" return Scene( - metadata=Metadata.fromdict(data_dict["metadata"], subschema_version), - sensors=sensors, - objects=objects, - frames=cls._frames_fromdict(data_dict["frames"], sensors, objects), + metadata=Metadata.from_json(json.openlabel.metadata), + sensors=_sensors_from_json(json.openlabel.streams, json.openlabel.coordinate_systems), + objects=_objects_from_json(json.openlabel.objects), + frames=_frames_from_json(json.openlabel.frames), ) - def asdict(self, calculate_pointers: bool = True) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this Scene. - calculate_pointers: bool, optional - If True, object_data_pointers and Object frame_intervals will be calculated. Default - is True. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "openlabel": _clean_dict( - { - "metadata": self.metadata.asdict(), - "streams": self._streams_asdict(self.sensors), - "coordinate_systems": self._coordinate_systems_asdict(self.sensors), - "objects": self._objects_asdict(self.objects, calculate_pointers), - "frames": self._frames_asdict(self.frames), - "frame_intervals": self._frame_intervals_asdict(self.frame_intervals), - } + def to_json(self) -> JSONScene: + """Export this scene into the RailLabel JSON format.""" + return JSONScene( + openlabel=JSONSceneContent( + metadata=self.metadata.to_json(), + streams={ + sensor_id: sensor.to_json()[0] for sensor_id, sensor in self.sensors.items() + }, + coordinate_systems=_coordinate_systems_to_json(self.sensors), + objects={ + obj_id: obj.to_json(obj_id, self.frames) for obj_id, obj in self.objects.items() + }, + frames={ + frame_id: frame.to_json(self.objects) for frame_id, frame in self.frames.items() + }, + frame_intervals=[ + fi.to_json() for fi in FrameInterval.from_frame_ids(list(self.frames.keys())) + ], ) - } - - # === Private Methods ========================================================================= - - # --- fromdict() ---------------------------- - - @classmethod - def _prepare_data(cls, data: dict) -> dict: - """Add optional fields to dict to simplify interaction. - - Parameters - ---------- - data : dict - JSON data. - - Returns - ------- - dict - Enhanced JSON data. - - """ - if "coordinate_systems" not in data["openlabel"]: - data["openlabel"]["coordinate_systems"] = {} - - if "streams" not in data["openlabel"]: - data["openlabel"]["streams"] = {} - - if "objects" not in data["openlabel"]: - data["openlabel"]["objects"] = {} + ) - if "frames" not in data["openlabel"]: - data["openlabel"]["frames"] = {} - return data["openlabel"] - - @classmethod - def _sensors_fromdict( - cls, streams_dict: dict, coordinate_systems_dict: dict - ) -> dict[str, Sensor]: - sensors = {} - - for stream_id in streams_dict: - sensors[stream_id] = Sensor.fromdict( - uid=stream_id, - cs_data_dict=coordinate_systems_dict[stream_id], - stream_data_dict=streams_dict[stream_id], - ) +def _sensors_from_json( + json_streams: dict[str, JSONStreamCamera | JSONStreamOther | JSONStreamRadar] | None, + json_coordinate_systems: dict[str, JSONCoordinateSystem] | None, +) -> dict[str, Camera | Lidar | Radar | GpsImu | OtherSensor]: + sensors: dict[str, Camera | Lidar | Radar | GpsImu | OtherSensor] = {} + if json_streams is None or json_coordinate_systems is None: return sensors - @classmethod - def _objects_fromdict(cls, object_dict: dict) -> dict[str, Object]: - return {uid: Object.fromdict(object_, uid) for uid, object_ in object_dict.items()} - - @classmethod - def _frames_fromdict( - cls, frames_dict: dict, sensors: dict[str, Sensor], objects: dict[str, Object] - ) -> dict[int, Frame]: - frames = {} - for frame_uid, frame_dict in frames_dict.items(): - frames[int(frame_uid)] = Frame.fromdict(frame_dict, objects, sensors) - - return frames + for sensor_id, json_stream in json_streams.items(): + json_coordinate_system = json_coordinate_systems[sensor_id] - # --- asdict() ------------------------------ + if isinstance(json_stream, JSONStreamCamera): + sensors[sensor_id] = Camera.from_json(json_stream, json_coordinate_system) - def _streams_asdict(self, sensors: dict[str, Sensor]) -> dict: - return {uid: sensor.asdict()["stream"] for uid, sensor in sensors.items()} + if isinstance(json_stream, JSONStreamRadar): + sensors[sensor_id] = Radar.from_json(json_stream, json_coordinate_system) - def _coordinate_systems_asdict(self, sensors: dict[str, Sensor]) -> dict[str, Any] | None: - if len(sensors) == 0: - return None + if isinstance(json_stream, JSONStreamOther) and json_stream.type == "lidar": + sensors[sensor_id] = Lidar.from_json(json_stream, json_coordinate_system) - coordinate_systems: dict[str, Any] = { - "base": {"type": "local", "parent": "", "children": []} - } + if isinstance(json_stream, JSONStreamOther) and json_stream.type == "gps_imu": + sensors[sensor_id] = GpsImu.from_json(json_stream, json_coordinate_system) - for uid, sensor in sensors.items(): - coordinate_systems[uid] = sensor.asdict()["coordinate_system"] - coordinate_systems["base"]["children"].append(uid) + if isinstance(json_stream, JSONStreamOther) and json_stream.type == "other": + sensors[sensor_id] = OtherSensor.from_json(json_stream, json_coordinate_system) - return coordinate_systems + return sensors - def _objects_asdict(self, objects: dict[str, Object], calculate_pointers: bool) -> dict: - if calculate_pointers: - return {str(uid): object_.asdict(self.frames) for uid, object_ in objects.items()} - return {str(uid): object_.asdict() for uid, object_ in objects.items()} - def _frames_asdict(self, frames: dict[int, Frame]) -> dict: - return {str(uid): frame.asdict() for uid, frame in frames.items()} +def _objects_from_json(json_objects: dict[UUID, JSONObject] | None) -> dict[UUID, Object]: + if json_objects is None: + return {} - def _frame_intervals_asdict(self, frame_intervals: list[FrameInterval]) -> list[dict]: - return [fi.asdict() for fi in frame_intervals] + return {obj_id: Object.from_json(json_obj) for obj_id, json_obj in json_objects.items()} -def _clean_dict(input_dict: dict) -> dict: - """Remove all fields from a dict that are None or have a length of 0.""" - empty_keys = [] - for key, value in input_dict.items(): - is_field_empty = value is None or (hasattr(value, "__len__") and len(value) == 0) +def _frames_from_json(json_frames: dict[int, JSONFrame] | None) -> dict[int, Frame]: + if json_frames is None: + return {} - if is_field_empty: - empty_keys.append(key) + return {frame_id: Frame.from_json(json_frame) for frame_id, json_frame in json_frames.items()} - for key in empty_keys: - del input_dict[key] - return input_dict +def _coordinate_systems_to_json( + sensors: dict[str, Camera | Lidar | Radar | GpsImu | OtherSensor], +) -> dict[str, JSONCoordinateSystem]: + json_coordinate_systems = { + sensor_id: sensor.to_json()[1] for sensor_id, sensor in sensors.items() + } + json_coordinate_systems["base"] = JSONCoordinateSystem( + parent="", + type="local", + pose_wrt_parent=None, + children=list(json_coordinate_systems.keys()), + ) + return json_coordinate_systems diff --git a/raillabel/format/seg3d.py b/raillabel/format/seg3d.py index 369976f..31dd7c0 100644 --- a/raillabel/format/seg3d.py +++ b/raillabel/format/seg3d.py @@ -4,85 +4,49 @@ from __future__ import annotations from dataclasses import dataclass +from uuid import UUID -from ._object_annotation import _ObjectAnnotation -from .object import Object +from raillabel.json_format import JSONVec +from ._attributes import _attributes_from_json, _attributes_to_json -@dataclass -class Seg3d(_ObjectAnnotation): - """The 3D segmentation of a lidar pointcloud. - - Parameters - ---------- - uid: str - This a string representing the unique universal identifier of the annotation. - point_ids: list of int - The list of point indices. - object: raillabel.format.Object - A reference to the object, this annotation belongs to. - sensor: raillabel.format.Sensor - A reference to the sensor, this annotation is labeled in. Default is None. - attributes: dict, optional - Attributes of the annotation. Dict keys are the name str of the attribute, values are the - attribute values. Default is {}. - Properties (read-only) - ---------------------- - name: str - Name of the annotation used by the VCD player for indexing in the object data pointers. - - """ +@dataclass +class Seg3d: + """The 3D segmentation of a lidar pointcloud.""" point_ids: list[int] + "The list of point indices." - OPENLABEL_ID = "vec" + object_id: UUID + "The unique identifyer of the real-life object, this annotation belongs to." - @classmethod - def fromdict(cls, data_dict: dict, sensors: dict, object: Object) -> Seg3d: - """Generate a Seg3d object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensors: dict - Dictionary containing all sensors for the scene. - object: raillabel.format.Object - Object this annotation belongs to. + sensor_id: str + "The unique identifyer of the sensor this annotation is labeled in." - Returns - ------- - annotation: Seg3d - Converted annotation. + attributes: dict[str, float | bool | str | list] + "Additional information associated with the annotation." - """ + @classmethod + def from_json(cls, json: JSONVec, object_id: UUID) -> Seg3d: + """Construct an instant of this class from RailLabel JSON data.""" return Seg3d( - uid=str(data_dict["uid"]), - point_ids=data_dict["val"], - object=object, - sensor=cls._coordinate_system_fromdict(data_dict, sensors), - attributes=cls._attributes_fromdict(data_dict), + point_ids=[int(point_id) for point_id in json.val], + object_id=object_id, + sensor_id=json.coordinate_system, + attributes=_attributes_from_json(json.attributes), ) - def asdict(self) -> dict: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr = self._annotation_required_fields_asdict() - - dict_repr["val"] = [int(pid) for pid in self.point_ids] - - dict_repr.update(self._annotation_optional_fields_asdict()) + def to_json(self, uid: UUID, object_type: str) -> JSONVec: + """Export this object into the RailLabel JSON format.""" + return JSONVec( + name=self.name(object_type), + val=self.point_ids, + coordinate_system=self.sensor_id, + uid=uid, + attributes=_attributes_to_json(self.attributes), + ) - return dict_repr + def name(self, object_type: str) -> str: + """Return the name of the annotation used for indexing in the object data pointers.""" + return f"{self.sensor_id}__vec__{object_type}" diff --git a/raillabel/format/sensor.py b/raillabel/format/sensor.py deleted file mode 100644 index 6bdf5c9..0000000 --- a/raillabel/format/sensor.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass -from enum import Enum -from typing import Any - -from .intrinsics_pinhole import IntrinsicsPinhole -from .intrinsics_radar import IntrinsicsRadar -from .point3d import Point3d -from .quaternion import Quaternion -from .transform import Transform - - -@dataclass -class Sensor: - """A reference to a physical sensor on the train. - - A sensor in the devkit corresponds to one coordinate_system and one stream in the data format. - This distinction is set by the OpenLABEL standard, but is not relevant for our data. - Therefore, we decided to combine these fields. - - Parameters - ---------- - uid: str - This is the friendly name of the sensor as well as its identifier. Must be - unique. - extrinsics: raillabel.format.Transform, optional - The external calibration of the sensor defined by the 3D transform to the coordinate - system origin. Default is None. - intrinsics: raillabel.format.IntrinsicsPinhole or raillabel.format.IntrinsicsRadar, optional - The intrinsic calibration of the sensor. Default is None. - type: raillabel.format.SensorType, optional - Information about the kind of sensor. Default is None. - uri: str, optional - Name of the subdirectory containing the sensor files. Default is None. - description: str, optional - Description of the sensor. Default is None. - - """ - - uid: str - extrinsics: Transform | None = None - intrinsics: IntrinsicsPinhole | IntrinsicsRadar | None = None - type: SensorType | None = None - uri: str | None = None - description: str | None = None - - @classmethod - def fromdict(cls, uid: str, cs_data_dict: dict, stream_data_dict: dict) -> Sensor: - """Generate a Sensor object from a dict. - - Parameters - ---------- - uid: str - Unique identifier of the sensor. - cs_data_dict: dict - RailLabel format dict containing the data about the coordinate system. - stream_data_dict: dict - RailLabel format dict containing the data about the stream. - - Returns - ------- - sensor: raillabel.format.Sensor - Converted Sensor object. - - """ - return Sensor( - uid=uid, - extrinsics=cls._extrinsics_fromdict(cs_data_dict), - intrinsics=cls._intrinsics_fromdict( - stream_data_dict, cls._type_fromdict(stream_data_dict) - ), - type=cls._type_fromdict(stream_data_dict), - uri=stream_data_dict.get("uri"), - description=stream_data_dict.get("description"), - ) - - def asdict(self) -> dict: - """Export self as a dict compatible with the RailLabel schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - """ - return { - "coordinate_system": self._as_coordinate_system_dict(), - "stream": self._as_stream_dict(), - } - - def _as_coordinate_system_dict(self) -> dict[str, Any]: - coordinate_system_repr: dict[str, Any] = {"type": "sensor", "parent": "base"} - - if self.extrinsics is not None: - coordinate_system_repr["pose_wrt_parent"] = self.extrinsics.asdict() - - return coordinate_system_repr - - def _as_stream_dict(self) -> dict[str, Any]: - stream_repr: dict[str, Any] = {} - - if self.type is not None: - stream_repr["type"] = str(self.type.value) - - if self.uri is not None: - stream_repr["uri"] = str(self.uri) - - if self.description is not None: - stream_repr["description"] = str(self.description) - - if isinstance(self.intrinsics, IntrinsicsPinhole): - stream_repr["stream_properties"] = {"intrinsics_pinhole": self.intrinsics.asdict()} - - elif isinstance(self.intrinsics, IntrinsicsRadar): - stream_repr["stream_properties"] = {"intrinsics_radar": self.intrinsics.asdict()} - - return stream_repr - - @classmethod - def _extrinsics_fromdict(cls, data_dict: dict) -> Transform | None: - if "pose_wrt_parent" not in data_dict: - return None - - return Transform( - position=Point3d( - x=data_dict["pose_wrt_parent"]["translation"][0], - y=data_dict["pose_wrt_parent"]["translation"][1], - z=data_dict["pose_wrt_parent"]["translation"][2], - ), - quaternion=Quaternion( - x=data_dict["pose_wrt_parent"]["quaternion"][0], - y=data_dict["pose_wrt_parent"]["quaternion"][1], - z=data_dict["pose_wrt_parent"]["quaternion"][2], - w=data_dict["pose_wrt_parent"]["quaternion"][3], - ), - ) - - @classmethod - def _intrinsics_fromdict( - cls, data_dict: dict, sensor_type: SensorType | None - ) -> IntrinsicsPinhole | IntrinsicsRadar | None: - if "stream_properties" not in data_dict: - return None - - if sensor_type == SensorType.CAMERA: - if "intrinsics_pinhole" in data_dict["stream_properties"]: - return IntrinsicsPinhole.fromdict( - data_dict["stream_properties"]["intrinsics_pinhole"] - ) - - elif ( - sensor_type == SensorType.RADAR and "intrinsics_radar" in data_dict["stream_properties"] - ): - return IntrinsicsRadar.fromdict(data_dict["stream_properties"]["intrinsics_radar"]) - - return None - - @classmethod - def _type_fromdict(cls, data_dict: dict) -> SensorType | None: - if "type" not in data_dict: - return None - - return SensorType(data_dict["type"]) - - -class SensorType(Enum): - """Enumeration representing all possible sensor types.""" - - CAMERA = "camera" - LIDAR = "lidar" - RADAR = "radar" - GPS_IMU = "gps_imu" - OTHER = "other" diff --git a/raillabel/format/sensor_reference.py b/raillabel/format/sensor_reference.py index fa4d1ca..3a547c8 100644 --- a/raillabel/format/sensor_reference.py +++ b/raillabel/format/sensor_reference.py @@ -3,80 +3,36 @@ from __future__ import annotations -import decimal from dataclasses import dataclass -from typing import Any +from decimal import Decimal -from .sensor import Sensor +from raillabel.json_format import JSONStreamSync, JSONStreamSyncProperties, JSONStreamSyncTimestamp @dataclass class SensorReference: - """A reference to a sensor in a specific frame. + """A reference to a sensor in a specific frame.""" - Parameters - ---------- - sensor: raillabel.format.Sensor - The sensor this SensorReference corresponds to. - timestamp: decimal.Decimal - Timestamp containing the Unix epoch time of the sensor in a specific frame with up to - nanosecond precision. - uri: str, optional - URI to the file corresponding to the frame recording in the particular frame. Default is - None. + timestamp: Decimal + """Timestamp containing the Unix epoch time of the sensor in a specific frame with up to + nanosecond precision.""" - """ - - sensor: Sensor - timestamp: decimal.Decimal uri: str | None = None + "URI to the file corresponding to the frame recording in the particular frame." @classmethod - def fromdict(cls, data_dict: dict, sensor: Sensor) -> SensorReference: - """Generate a SensorReference object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - sensor: raillabel.format.Sensor - Sensor corresponding to this SensorReference. - - Returns - ------- - sensor_reference: raillabel.format.SensorReference - Converted SensorReference object. - - """ + def from_json(cls, json: JSONStreamSync) -> SensorReference: + """Construct an instant of this class from RailLabel JSON data.""" return SensorReference( - sensor=sensor, - timestamp=cls._timestamp_fromdict(data_dict["stream_properties"]), - uri=data_dict.get("uri"), + timestamp=Decimal(json.stream_properties.sync.timestamp), + uri=json.uri, ) - def asdict(self) -> dict[str, Any]: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - dict_repr: dict[str, Any] = { - "stream_properties": {"sync": {"timestamp": str(self.timestamp)}} - } - - if self.uri is not None: - dict_repr["uri"] = self.uri - - return dict_repr - - @classmethod - def _timestamp_fromdict(cls, data_dict: dict) -> decimal.Decimal: - return decimal.Decimal(data_dict["sync"]["timestamp"]) + def to_json(self) -> JSONStreamSync: + """Export this object into the RailLabel JSON format.""" + return JSONStreamSync( + stream_properties=JSONStreamSyncProperties( + sync=JSONStreamSyncTimestamp(timestamp=str(self.timestamp)), + ), + uri=self.uri, + ) diff --git a/raillabel/format/size2d.py b/raillabel/format/size2d.py index 01df3ed..5afe2a6 100644 --- a/raillabel/format/size2d.py +++ b/raillabel/format/size2d.py @@ -10,32 +10,17 @@ class Size2d: """The size of a rectangle in a 2d image.""" - x: int | float + x: float "The size along the x-axis." - y: int | float + y: float "The size along the y-axis." @classmethod - def from_json(cls, json: tuple[int | float, int | float]) -> Size2d: + def from_json(cls, json: tuple[float, float]) -> Size2d: """Construct an instant of this class from RailLabel JSON data.""" return Size2d(x=json[0], y=json[1]) - @classmethod - def fromdict(cls, data_dict: dict) -> Size2d: - """Generate a Size2d object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Size2d( - x=data_dict[0], - y=data_dict[1], - ) - - def asdict(self) -> list[float]: - """Export self as a dict compatible with the OpenLABEL schema.""" - return [float(self.x), float(self.y)] + def to_json(self) -> tuple[float, float]: + """Export this object into the RailLabel JSON format.""" + return (self.x, self.y) diff --git a/raillabel/format/size3d.py b/raillabel/format/size3d.py index f98a35b..7810689 100644 --- a/raillabel/format/size3d.py +++ b/raillabel/format/size3d.py @@ -8,39 +8,22 @@ @dataclass class Size3d: - """The 3D size of a cube. - - Parameters - ---------- - x: float or int - The size along the x-axis. - y: float or int - The size along the y-axis. - z: float or int - The size along the z-axis. - - """ + """The 3D size of a cube.""" x: float + "The size along the x-axis." + y: float + "The size along the y-axis." + z: float + "The size along the z-axis." @classmethod - def fromdict(cls, data_dict: dict) -> Size3d: - """Generate a Size3d object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Size3d( - x=data_dict[0], - y=data_dict[1], - z=data_dict[2], - ) - - def asdict(self) -> list[float]: - """Export self as a dict compatible with the OpenLABEL schema.""" - return [float(self.x), float(self.y), float(self.z)] + def from_json(cls, json: tuple[float, float, float]) -> Size3d: + """Construct an instant of this class from RailLabel JSON data.""" + return Size3d(x=json[0], y=json[1], z=json[2]) + + def to_json(self) -> tuple[float, float, float]: + """Export this object into the RailLabel JSON format.""" + return (self.x, self.y, self.z) diff --git a/raillabel/format/transform.py b/raillabel/format/transform.py index c8cd55a..defe708 100644 --- a/raillabel/format/transform.py +++ b/raillabel/format/transform.py @@ -15,50 +15,23 @@ class Transform: """A transformation between two coordinate systems.""" - position: Point3d + pos: Point3d "Translation with regards to the parent coordinate system." - quaternion: Quaternion + quat: Quaternion "Rotation quaternion with regards to the parent coordinate system." @classmethod def from_json(cls, json: JSONTransformData) -> Transform: """Construct an instant of this class from RailLabel JSON data.""" return Transform( - position=Point3d.from_json(json.translation), - quaternion=Quaternion.from_json(json.quaternion), + pos=Point3d.from_json(json.translation), + quat=Quaternion.from_json(json.quaternion), ) - @classmethod - def fromdict(cls, data_dict: dict) -> Transform: - """Generate a Transform object from a dict. - - Parameters - ---------- - data_dict: dict - RailLabel format snippet containing the relevant data. - - """ - return Transform( - position=Point3d.fromdict(data_dict["translation"]), - quaternion=Quaternion.fromdict(data_dict["quaternion"]), + def to_json(self) -> JSONTransformData: + """Export this object into the RailLabel JSON format.""" + return JSONTransformData( + translation=self.pos.to_json(), + quaternion=self.quat.to_json(), ) - - def asdict(self) -> dict[str, list[float]]: - """Export self as a dict compatible with the OpenLABEL schema. - - Returns - ------- - dict_repr: dict - Dict representation of this class instance. - - Raises - ------ - ValueError - if an attribute can not be converted to the type required by the OpenLabel schema. - - """ - return { - "translation": self.position.asdict(), - "quaternion": self.quaternion.asdict(), - } diff --git a/raillabel/json_format/__init__.py b/raillabel/json_format/__init__.py index 4093a69..9ad9f51 100644 --- a/raillabel/json_format/__init__.py +++ b/raillabel/json_format/__init__.py @@ -8,13 +8,13 @@ from .coordinate_system import JSONCoordinateSystem from .cuboid import JSONCuboid from .element_data_pointer import JSONElementDataPointer -from .frame import JSONFrame +from .frame import JSONFrame, JSONFrameData, JSONFrameProperties from .frame_interval import JSONFrameInterval from .metadata import JSONMetadata from .num import JSONNum from .num_attribute import JSONNumAttribute from .object import JSONObject -from .object_data import JSONObjectData +from .object_data import JSONAnnotations, JSONObjectData from .poly2d import JSONPoly2d from .poly3d import JSONPoly3d from .scene import JSONScene, JSONSceneContent @@ -28,6 +28,7 @@ from .vec_attribute import JSONVecAttribute __all__ = [ + "JSONAnnotations", "JSONAttributes", "JSONBbox", "JSONBooleanAttribute", @@ -36,6 +37,8 @@ "JSONElementDataPointer", "JSONFrameInterval", "JSONFrame", + "JSONFrameData", + "JSONFrameProperties", "JSONMetadata", "JSONNumAttribute", "JSONNum", diff --git a/raillabel/json_format/_json_format_base.py b/raillabel/json_format/_json_format_base.py new file mode 100644 index 0000000..5095d77 --- /dev/null +++ b/raillabel/json_format/_json_format_base.py @@ -0,0 +1,8 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from pydantic import BaseModel + + +class _JSONFormatBase(BaseModel, extra="forbid"): + pass diff --git a/raillabel/json_format/attributes.py b/raillabel/json_format/attributes.py index 976c44d..30846e6 100644 --- a/raillabel/json_format/attributes.py +++ b/raillabel/json_format/attributes.py @@ -3,15 +3,14 @@ from __future__ import annotations -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .boolean_attribute import JSONBooleanAttribute from .num_attribute import JSONNumAttribute from .text_attribute import JSONTextAttribute from .vec_attribute import JSONVecAttribute -class JSONAttributes(BaseModel): +class JSONAttributes(_JSONFormatBase): """Attributes is the alias of element data that can be nested inside geometric object data. For example, a certain bounding box can have attributes related to its score, visibility, etc. diff --git a/raillabel/json_format/bbox.py b/raillabel/json_format/bbox.py index c1df9f1..7fd9f6a 100644 --- a/raillabel/json_format/bbox.py +++ b/raillabel/json_format/bbox.py @@ -5,12 +5,11 @@ from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .attributes import JSONAttributes -class JSONBbox(BaseModel): +class JSONBbox(_JSONFormatBase): """A 2D bounding box is defined as a 4-dimensional vector [x, y, w, h]. [x, y] is the center of the bounding box and [w, h] represent the width (horizontal, @@ -24,7 +23,7 @@ class JSONBbox(BaseModel): val: tuple[float, float, float, float] "The array of 4 values that define the [x, y, w, h] values of the bbox." - coordinate_system: str | None = None + coordinate_system: str "Name of the coordinate system in respect of which this object data is expressed." uid: UUID | None = None diff --git a/raillabel/json_format/boolean_attribute.py b/raillabel/json_format/boolean_attribute.py index 04db175..a41ca11 100644 --- a/raillabel/json_format/boolean_attribute.py +++ b/raillabel/json_format/boolean_attribute.py @@ -3,15 +3,15 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONBooleanAttribute(BaseModel): +class JSONBooleanAttribute(_JSONFormatBase): """A boolean attribute.""" - val: bool - "The boolean value." - - name: str | None = None + name: str """Friendly identifier describing the attribute. Used to track the attribute throughout annotations and frames.""" + + val: bool + "The boolean value." diff --git a/raillabel/json_format/coordinate_system.py b/raillabel/json_format/coordinate_system.py index e1d664a..899581d 100644 --- a/raillabel/json_format/coordinate_system.py +++ b/raillabel/json_format/coordinate_system.py @@ -5,12 +5,11 @@ from typing import Literal -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .transform_data import JSONTransformData -class JSONCoordinateSystem(BaseModel): +class JSONCoordinateSystem(_JSONFormatBase): """A 3D reference frame.""" parent: Literal["base", ""] diff --git a/raillabel/json_format/cuboid.py b/raillabel/json_format/cuboid.py index e07032f..0831add 100644 --- a/raillabel/json_format/cuboid.py +++ b/raillabel/json_format/cuboid.py @@ -5,12 +5,11 @@ from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .attributes import JSONAttributes -class JSONCuboid(BaseModel): +class JSONCuboid(_JSONFormatBase): """A cuboid or 3D bounding box. It is defined by the position of its center, the rotation in 3D, and its dimensions. @@ -26,7 +25,7 @@ class JSONCuboid(BaseModel): encodes the quaternion that encode the rotation, and (sx, sy, sz) are the dimensions of the cuboid in its object coordinate system""" - coordinate_system: str | None = None + coordinate_system: str "Name of the coordinate system in respect of which this object data is expressed." uid: UUID | None = None diff --git a/raillabel/json_format/element_data_pointer.py b/raillabel/json_format/element_data_pointer.py index 48cfa14..d0dea1d 100644 --- a/raillabel/json_format/element_data_pointer.py +++ b/raillabel/json_format/element_data_pointer.py @@ -5,12 +5,11 @@ from typing import Literal -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .frame_interval import JSONFrameInterval -class JSONElementDataPointer(BaseModel): +class JSONElementDataPointer(_JSONFormatBase): """A pointer to element data of elements. It is indexed by 'name', and containing information about the element data type, for example, diff --git a/raillabel/json_format/frame.py b/raillabel/json_format/frame.py index 797baec..2ec6334 100644 --- a/raillabel/json_format/frame.py +++ b/raillabel/json_format/frame.py @@ -6,14 +6,13 @@ from decimal import Decimal from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .num import JSONNum from .object_data import JSONObjectData from .stream_sync import JSONStreamSync -class JSONFrame(BaseModel): +class JSONFrame(_JSONFormatBase): """A frame is a container of dynamic, timewise, information.""" frame_properties: JSONFrameProperties | None = None @@ -24,7 +23,7 @@ class JSONFrame(BaseModel): strings containing 32 bytes UUIDs. Object values contain an 'object_data' JSON object.""" -class JSONFrameProperties(BaseModel): +class JSONFrameProperties(_JSONFormatBase): """Container of frame information other than annotations.""" timestamp: Decimal | str | None = None @@ -38,7 +37,7 @@ class JSONFrameProperties(BaseModel): "Additional data to describe attributes of the frame (like GPS position)." -class JSONFrameData(BaseModel): +class JSONFrameData(_JSONFormatBase): """Additional data to describe attributes of the frame (like GPS position).""" num: list[JSONNum] | None = None diff --git a/raillabel/json_format/frame_interval.py b/raillabel/json_format/frame_interval.py index b56cbe9..984065c 100644 --- a/raillabel/json_format/frame_interval.py +++ b/raillabel/json_format/frame_interval.py @@ -3,10 +3,10 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONFrameInterval(BaseModel): +class JSONFrameInterval(_JSONFormatBase): """A frame interval defines a starting and ending frame number as a closed interval. That means the interval includes the limit frame numbers. diff --git a/raillabel/json_format/metadata.py b/raillabel/json_format/metadata.py index a24e117..2862a25 100644 --- a/raillabel/json_format/metadata.py +++ b/raillabel/json_format/metadata.py @@ -8,7 +8,7 @@ from pydantic import BaseModel -class JSONMetadata(BaseModel): +class JSONMetadata(BaseModel, extra="allow"): """Metadata about the annotation file itself.""" schema_version: Literal["1.0.0"] @@ -29,5 +29,8 @@ class JSONMetadata(BaseModel): tagged_file: str | None = None "File name or URI of the data file being tagged." + annotator: str | None = None + "The person or organization responsible for annotating the file." + comment: str | None = None "Additional information or description about the annotation content." diff --git a/raillabel/json_format/num.py b/raillabel/json_format/num.py index 75d08d4..ce67b9f 100644 --- a/raillabel/json_format/num.py +++ b/raillabel/json_format/num.py @@ -5,12 +5,10 @@ from uuid import UUID -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -from .attributes import JSONAttributes - -class JSONNum(BaseModel): +class JSONNum(_JSONFormatBase): """A number.""" name: str @@ -25,5 +23,3 @@ class JSONNum(BaseModel): uid: UUID | None = None "This is a string encoding the Universal Unique identifyer of the annotation." - - attributes: JSONAttributes | None = None diff --git a/raillabel/json_format/num_attribute.py b/raillabel/json_format/num_attribute.py index e70e69b..edfdfcd 100644 --- a/raillabel/json_format/num_attribute.py +++ b/raillabel/json_format/num_attribute.py @@ -3,15 +3,15 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONNumAttribute(BaseModel): +class JSONNumAttribute(_JSONFormatBase): """A number attribute.""" - name: str | None = None + name: str """Friendly identifier describing the attribute. Used to track the attribute throughout annotations and frames.""" - val: int | float + val: float "The number value." diff --git a/raillabel/json_format/object.py b/raillabel/json_format/object.py index 091843a..777214e 100644 --- a/raillabel/json_format/object.py +++ b/raillabel/json_format/object.py @@ -3,13 +3,12 @@ from __future__ import annotations -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .element_data_pointer import JSONElementDataPointer from .frame_interval import JSONFrameInterval -class JSONObject(BaseModel): +class JSONObject(_JSONFormatBase): """An object is the main type of annotation element. Object is designed to represent spatiotemporal entities, such as physical objects in the real diff --git a/raillabel/json_format/object_data.py b/raillabel/json_format/object_data.py index bffa98a..68d4ba9 100644 --- a/raillabel/json_format/object_data.py +++ b/raillabel/json_format/object_data.py @@ -3,8 +3,7 @@ from __future__ import annotations -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .bbox import JSONBbox from .cuboid import JSONCuboid from .poly2d import JSONPoly2d @@ -12,9 +11,15 @@ from .vec import JSONVec -class JSONObjectData(BaseModel): +class JSONObjectData(_JSONFormatBase): """Container of annotations of an object in a frame.""" + object_data: JSONAnnotations + + +class JSONAnnotations(_JSONFormatBase): + """Container of the annotations by type.""" + bbox: list[JSONBbox] | None = None cuboid: list[JSONCuboid] | None = None poly2d: list[JSONPoly2d] | None = None diff --git a/raillabel/json_format/poly2d.py b/raillabel/json_format/poly2d.py index d8f687c..3a0ddcd 100644 --- a/raillabel/json_format/poly2d.py +++ b/raillabel/json_format/poly2d.py @@ -6,12 +6,11 @@ from typing import Literal from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .attributes import JSONAttributes -class JSONPoly2d(BaseModel): +class JSONPoly2d(_JSONFormatBase): """A 2D polyline defined as a sequence of 2D points.""" name: str @@ -30,7 +29,7 @@ class JSONPoly2d(BaseModel): MODE_POLY2D_ABSOLUTE means that any point defined by an x-value followed by a y-value is the absolute position.""" - coordinate_system: str | None = None + coordinate_system: str "Name of the coordinate system in respect of which this object data is expressed." uid: UUID | None = None diff --git a/raillabel/json_format/poly3d.py b/raillabel/json_format/poly3d.py index 685f088..b185c93 100644 --- a/raillabel/json_format/poly3d.py +++ b/raillabel/json_format/poly3d.py @@ -5,12 +5,11 @@ from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .attributes import JSONAttributes -class JSONPoly3d(BaseModel): +class JSONPoly3d(_JSONFormatBase): """A 3D polyline defined as a sequence of 3D points.""" name: str @@ -24,7 +23,7 @@ class JSONPoly3d(BaseModel): """A boolean that defines whether the polyline is closed or not. In case it is closed, it is assumed that the last point of the sequence is connected with the first one.""" - coordinate_system: str | None = None + coordinate_system: str "Name of the coordinate system in respect of which this object data is expressed." uid: UUID | None = None diff --git a/raillabel/json_format/scene.py b/raillabel/json_format/scene.py index 65326ec..1a618b0 100644 --- a/raillabel/json_format/scene.py +++ b/raillabel/json_format/scene.py @@ -5,8 +5,7 @@ from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .coordinate_system import JSONCoordinateSystem from .frame import JSONFrame from .frame_interval import JSONFrameInterval @@ -17,13 +16,13 @@ from .stream_radar import JSONStreamRadar -class JSONScene(BaseModel): +class JSONScene(_JSONFormatBase): """Root RailLabel object.""" openlabel: JSONSceneContent -class JSONSceneContent(BaseModel): +class JSONSceneContent(_JSONFormatBase): """Container of all scene content.""" metadata: JSONMetadata diff --git a/raillabel/json_format/stream_camera.py b/raillabel/json_format/stream_camera.py index b8f27ec..fbcd980 100644 --- a/raillabel/json_format/stream_camera.py +++ b/raillabel/json_format/stream_camera.py @@ -5,10 +5,10 @@ from typing import Literal -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONStreamCamera(BaseModel): +class JSONStreamCamera(_JSONFormatBase): """A stream describes the source of a data sequence, usually a sensor. This specific object contains the intrinsics of a camera sensor. @@ -27,13 +27,13 @@ class JSONStreamCamera(BaseModel): "Description of the stream." -class JSONStreamCameraProperties(BaseModel): +class JSONStreamCameraProperties(_JSONFormatBase): """Intrinsic calibration of the stream.""" intrinsics_pinhole: JSONIntrinsicsPinhole -class JSONIntrinsicsPinhole(BaseModel): +class JSONIntrinsicsPinhole(_JSONFormatBase): """JSON object defining an instance of the intrinsic parameters of a pinhole camera.""" camera_matrix: tuple[ diff --git a/raillabel/json_format/stream_other.py b/raillabel/json_format/stream_other.py index a1d47ba..27fee1b 100644 --- a/raillabel/json_format/stream_other.py +++ b/raillabel/json_format/stream_other.py @@ -5,10 +5,10 @@ from typing import Literal -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONStreamOther(BaseModel): +class JSONStreamOther(_JSONFormatBase): """A stream describes the source of a data sequence, usually a sensor. This specific object describes a sensor without intrinsic calibration. diff --git a/raillabel/json_format/stream_radar.py b/raillabel/json_format/stream_radar.py index 3c4a19c..67dfceb 100644 --- a/raillabel/json_format/stream_radar.py +++ b/raillabel/json_format/stream_radar.py @@ -5,10 +5,10 @@ from typing import Literal -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONStreamRadar(BaseModel): +class JSONStreamRadar(_JSONFormatBase): """A stream describes the source of a data sequence, usually a sensor. This specific object contains the intrinsics of a radar sensor. @@ -27,13 +27,13 @@ class JSONStreamRadar(BaseModel): "Description of the stream." -class JSONStreamRadarProperties(BaseModel): +class JSONStreamRadarProperties(_JSONFormatBase): """Intrinsic calibration of the stream.""" intrinsics_radar: JSONIntrinsicsRadar -class JSONIntrinsicsRadar(BaseModel): +class JSONIntrinsicsRadar(_JSONFormatBase): """JSON object defining an instance of the intrinsic parameters of a radar.""" resolution_px_per_m: float diff --git a/raillabel/json_format/stream_sync.py b/raillabel/json_format/stream_sync.py index 2757924..0c7c95f 100644 --- a/raillabel/json_format/stream_sync.py +++ b/raillabel/json_format/stream_sync.py @@ -5,23 +5,23 @@ from decimal import Decimal -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONStreamSync(BaseModel): +class JSONStreamSync(_JSONFormatBase): """Syncronization information of a stream in a frame.""" stream_properties: JSONStreamSyncProperties uri: str | None = None -class JSONStreamSyncProperties(BaseModel): +class JSONStreamSyncProperties(_JSONFormatBase): """The sync information.""" sync: JSONStreamSyncTimestamp -class JSONStreamSyncTimestamp(BaseModel): +class JSONStreamSyncTimestamp(_JSONFormatBase): """The timestamp of a stream sync.""" timestamp: Decimal | str diff --git a/raillabel/json_format/text_attribute.py b/raillabel/json_format/text_attribute.py index 221b63a..ddccbbc 100644 --- a/raillabel/json_format/text_attribute.py +++ b/raillabel/json_format/text_attribute.py @@ -3,13 +3,13 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONTextAttribute(BaseModel): +class JSONTextAttribute(_JSONFormatBase): """A text attribute.""" - name: str | None = None + name: str """Friendly identifier describing the attribute. Used to track the attribute throughout annotations and frames.""" diff --git a/raillabel/json_format/transform_data.py b/raillabel/json_format/transform_data.py index edf7c87..c23addb 100644 --- a/raillabel/json_format/transform_data.py +++ b/raillabel/json_format/transform_data.py @@ -3,10 +3,10 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONTransformData(BaseModel): +class JSONTransformData(_JSONFormatBase): """The translation and rotation of one coordinate system to another.""" translation: tuple[float, float, float] diff --git a/raillabel/json_format/vec.py b/raillabel/json_format/vec.py index 7a28345..1d81162 100644 --- a/raillabel/json_format/vec.py +++ b/raillabel/json_format/vec.py @@ -6,12 +6,11 @@ from typing import Literal from uuid import UUID -from pydantic import BaseModel - +from ._json_format_base import _JSONFormatBase from .attributes import JSONAttributes -class JSONVec(BaseModel): +class JSONVec(_JSONFormatBase): """A vector (list) of numbers.""" name: str @@ -21,7 +20,7 @@ class JSONVec(BaseModel): val: list[float] "The numerical values of the vector (list) of numbers." - coordinate_system: str | None = None + coordinate_system: str "Name of the coordinate system in respect of which this object data is expressed." uid: UUID | None = None diff --git a/raillabel/json_format/vec_attribute.py b/raillabel/json_format/vec_attribute.py index 6540475..9dc1104 100644 --- a/raillabel/json_format/vec_attribute.py +++ b/raillabel/json_format/vec_attribute.py @@ -3,15 +3,15 @@ from __future__ import annotations -from pydantic import BaseModel +from ._json_format_base import _JSONFormatBase -class JSONVecAttribute(BaseModel): +class JSONVecAttribute(_JSONFormatBase): """A vec attribute.""" - name: str | None = None + name: str """Friendly identifier describing the attribute. Used to track the attribute throughout annotations and frames.""" - val: list[int | float | str] + val: list[float | str] "The value vector of the attribute." diff --git a/raillabel/load/load.py b/raillabel/load/load.py index f45105d..1011347 100644 --- a/raillabel/load/load.py +++ b/raillabel/load/load.py @@ -7,23 +7,11 @@ from pathlib import Path from raillabel.format import Scene +from raillabel.json_format import JSONScene def load(path: Path | str) -> Scene: - """Load an annotation file of any supported type. - - Parameters - ---------- - path: str - Path to the annotation file. - - Returns - ------- - scene: raillabel.Scene - Scene with the loaded data. - - """ - with Path(path).open() as scene_file: - raw_scene = json.load(scene_file) - - return Scene.fromdict(raw_scene) + """Load an annotation file as a scene.""" + with Path(path).open() as annotation_file: + json_data = json.load(annotation_file) + return Scene.from_json(JSONScene(**json_data)) diff --git a/raillabel/save/save.py b/raillabel/save/save.py index 3adfef5..dbe62fc 100644 --- a/raillabel/save/save.py +++ b/raillabel/save/save.py @@ -3,34 +3,17 @@ from __future__ import annotations -import json from pathlib import Path from raillabel.format import Scene def save(scene: Scene, path: Path | str, prettify_json: bool = False) -> None: - """Save a raillabel.Scene in a JSON file. - - Parameters - ---------- - scene: raillabel.Scene - Scene, which should be saved. - path: str - Path to the file location, that should be used for saving. - save_path: str - Path to the JSON file. - prettify_json: bool, optional - If true, the JSON is saved with linebreaks and indents. This increases readibility but - also the file size. Default is False. - - """ - path = Path(path) - - data = scene.asdict() - - with Path(path).open("w") as save_file: - if prettify_json: - json.dump(data, save_file, indent=4) - else: - json.dump(data, save_file) + """Save a raillabel.Scene to a JSON file.""" + if prettify_json: + json_data = scene.to_json().model_dump_json(indent=4) + else: + json_data = scene.to_json().model_dump_json() + + with Path(path).open("w") as scene_file: + scene_file.write(json_data) diff --git a/tests/__test_assets__/openlabel_v1_schema.json b/tests/__test_assets__/openlabel_v1_schema.json index 45997a6..8d0021e 100644 --- a/tests/__test_assets__/openlabel_v1_schema.json +++ b/tests/__test_assets__/openlabel_v1_schema.json @@ -24,12 +24,12 @@ "description": "Name of the action. It is a friendly name and not used for indexing.", "type": "string" }, - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this action is defined.", "type": "string" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "type": { "description": "The type of an action defines the class the action corresponds to.", @@ -253,12 +253,12 @@ "description": "Name of the context. It is a friendly name and not used for indexing.", "type": "string" }, - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this context is defined.", "type": "string" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "type": { "description": "The type of a context defines the class the context corresponds to.", @@ -469,12 +469,12 @@ "description": "Name of the event. It is a friendly name and not used for indexing.", "type": "string" }, - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this event is defined.", "type": "string" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "type": { "description": "The type of an event defines the class the event corresponds to.", @@ -907,12 +907,12 @@ "object_data_pointers": { "$ref": "#/definitions/element_data_pointers" }, - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this object is defined.", "type": "string" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "type": { "description": "The type of an object defines the class the object corresponds to.", @@ -1427,7 +1427,7 @@ "description": "Name of the relation. It is a friendly name and not used for indexing.", "type": "string" }, - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this relation is defined.", "type": "string" }, @@ -1445,8 +1445,8 @@ }, "type": "array" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "type": { "description": "The type of a relation defines the class the predicated of the relation corresponds to.", @@ -1461,8 +1461,8 @@ ], "type": "object" }, - "resource_uid": { - "description": "This is a JSON object that contains links to external resources. Resource_uid keys are strings containing numerical UIDs or 32 bytes UUIDs. Resource_uid values are strings describing the identifier of the element in the external resource.", + "resource_id": { + "description": "This is a JSON object that contains links to external resources. Resource_id keys are strings containing numerical UIDs or 32 bytes UUIDs. Resource_id values are strings describing the identifier of the element in the external resource.", "patternProperties": { "^(-?[0-9]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$": { "type": "string" @@ -1652,12 +1652,12 @@ "additionalProperties": true, "description": "A tag is a special type of label that can be attached to any type of content, such as images, data containers, folders. In ASAM OpenLABEL the main purpose of a tag is to allow adding metadata to scenario descriptions.", "properties": { - "ontology_uid": { + "ontology_id": { "description": "This is the UID of the ontology where the type of this tag is defined.", "type": "string" }, - "resource_uid": { - "$ref": "#/definitions/resource_uid" + "resource_id": { + "$ref": "#/definitions/resource_id" }, "tag_data": { "$ref": "#/definitions/tag_data" @@ -1668,7 +1668,7 @@ } }, "required": [ - "ontology_uid", + "ontology_id", "type" ], "type": "object" diff --git a/tests/test_raillabel/format/conftest.py b/tests/test_raillabel/format/conftest.py index bc4a860..4f75bde 100644 --- a/tests/test_raillabel/format/conftest.py +++ b/tests/test_raillabel/format/conftest.py @@ -1,60 +1,38 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 - -from .test_attributes import ( - attributes_multiple_types, - attributes_multiple_types_dict, - attributes_single_type, - attributes_single_type_dict, -) -from .test_bbox import bbox, bbox_dict, bbox_train, bbox_train_dict -from .test_cuboid import cuboid, cuboid_dict -from .test_element_data_pointer import ( - element_data_pointer_full, - element_data_pointer_full_dict, - element_data_pointer_minimal, - element_data_pointer_minimal_dict, -) -from .test_frame import frame, frame_dict -from .test_frame_interval import frame_interval, frame_interval_dict -from .test_intrinsics_pinhole import intrinsics_pinhole, intrinsics_pinhole_dict -from .test_intrinsics_radar import intrinsics_radar, intrinsics_radar_dict -from .test_metadata import ( - metadata_full, - metadata_full_dict, - metadata_minimal, - metadata_minimal_dict, -) -from .test_num import num, num_dict +from .test_attributes import attributes_multiple_types, attributes_multiple_types_json +from .test_bbox import bbox, bbox_json, bbox_id +from .test_camera import camera, camera_json +from .test_cuboid import cuboid, cuboid_json, cuboid_id +from .test_frame import frame, frame_json +from .test_frame_interval import frame_interval, frame_interval_json +from .test_intrinsics_pinhole import intrinsics_pinhole, intrinsics_pinhole_json +from .test_intrinsics_radar import intrinsics_radar, intrinsics_radar_json +from .test_lidar import lidar, lidar_json +from .test_metadata import metadata, metadata_json +from .test_num import num, num_json, num_id from .test_object import ( - object_person, - object_person_dict, - object_train, - object_train_dict, objects, - objects_dict, + object_person, + object_person_json, + object_person_id, + object_track, + object_track_json, + object_track_id, ) -from .test_object_annotation import all_annotations -from .test_object_data import object_data_person_dict, object_data_train_dict -from .test_point2d import point2d, point2d_another, point2d_another_dict, point2d_dict -from .test_point3d import point3d, point3d_another, point3d_another_dict, point3d_dict -from .test_poly2d import poly2d, poly2d_dict -from .test_poly3d import poly3d, poly3d_dict -from .test_quaternion import quaternion, quaternion_dict -from .test_scene import scene, scene_dict -from .test_seg3d import seg3d, seg3d_dict -from .test_sensor import ( - coordinate_systems_dict, - sensor_camera, - sensor_camera_dict, - sensor_lidar, - sensor_lidar_dict, - sensor_radar, - sensor_radar_dict, - sensors, - streams_dict, +from .test_point2d import point2d, point2d_json, another_point2d, another_point2d_json +from .test_point3d import point3d, point3d_json, another_point3d, another_point3d_json +from .test_poly2d import poly2d, poly2d_json, poly2d_id +from .test_poly3d import poly3d, poly3d_json, poly3d_id +from .test_quaternion import quaternion, quaternion_json +from .test_radar import radar, radar_json +from .test_size2d import size2d, size2d_json +from .test_size3d import size3d, size3d_json +from .test_seg3d import seg3d, seg3d_json, seg3d_id +from .test_sensor_reference import ( + another_sensor_reference, + another_sensor_reference_json, + sensor_reference, + sensor_reference_json, ) -from .test_sensor_reference import sensor_reference_camera, sensor_reference_camera_dict -from .test_size2d import size2d, size2d_dict -from .test_size3d import size3d, size3d_dict -from .test_transform import transform, transform_dict +from .test_transform import transform, transform_json diff --git a/tests/test_raillabel/format/test_attributes.py b/tests/test_raillabel/format/test_attributes.py index b72fac8..604652e 100644 --- a/tests/test_raillabel/format/test_attributes.py +++ b/tests/test_raillabel/format/test_attributes.py @@ -3,124 +3,95 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -from raillabel.format import _ObjectAnnotation +from raillabel.json_format import ( + JSONAttributes, + JSONBooleanAttribute, + JSONNumAttribute, + JSONTextAttribute, + JSONVecAttribute, +) +from raillabel.format._attributes import ( + _attributes_from_json, + _attributes_to_json, + UnsupportedAttributeTypeError, +) # == Fixtures ========================= @pytest.fixture -def attributes_single_type_dict() -> dict: - return { - "text": [ - {"name": "test_text_attr0", "val": "test_text_attr0_val"}, - {"name": "test_text_attr1", "val": "test_text_attr1_val"}, - ] - } - - -@pytest.fixture -def attributes_single_type() -> dict: - return { - "test_text_attr0": "test_text_attr0_val", - "test_text_attr1": "test_text_attr1_val", - } - - -@pytest.fixture -def attributes_multiple_types_dict() -> dict: - return { - "text": [{"name": "text_attr", "val": "text_val"}], - "num": [{"name": "num_attr", "val": 0}], - "boolean": [{"name": "bool_attr", "val": True}], - "vec": [{"name": "vec_attr", "val": [0, 1, 2]}], - } +def attributes_multiple_types_json() -> JSONAttributes: + return JSONAttributes( + boolean=[ + JSONBooleanAttribute(name="has_red_hat", val=True), + JSONBooleanAttribute(name="has_green_hat", val=False), + ], + num=[JSONNumAttribute(name="number_of_red_clothing_items", val=2)], + text=[JSONTextAttribute(name="color_of_hat", val="red")], + vec=[ + JSONVecAttribute( + name="clothing_items", val=["red_hat", "brown_coat", "black_pants", "red_shoes"] + ) + ], + ) @pytest.fixture def attributes_multiple_types() -> dict: return { - "text_attr": "text_val", - "num_attr": 0, - "bool_attr": True, - "vec_attr": [0, 1, 2], + "has_red_hat": True, + "has_green_hat": False, + "number_of_red_clothing_items": 2, + "color_of_hat": "red", + "clothing_items": ["red_hat", "brown_coat", "black_pants", "red_shoes"], } # == Tests ============================ -def test_fromdict__single_type(): - attributes_dict = { - "attributes": { - "text": [ - {"name": "test_text_attr0", "val": "test_text_attr0_val"}, - {"name": "test_text_attr1", "val": "test_text_attr1_val"}, - ] - } - } +def test_attributes_from_json__none(): + actual = _attributes_from_json(None) + assert actual == {} - assert _ObjectAnnotation._attributes_fromdict(attributes_dict) == { - "test_text_attr0": "test_text_attr0_val", - "test_text_attr1": "test_text_attr1_val", - } +def test_attributes_from_json__empty(): + json_attributes = JSONAttributes( + boolean=None, + num=None, + text=None, + vec=None, + ) -def test_fromdict__multiple_types(): - attributes_dict = { - "attributes": { - "text": [{"name": "text_attr", "val": "text_val"}], - "num": [{"name": "num_attr", "val": 0}], - "boolean": [{"name": "bool_attr", "val": True}], - "vec": [{"name": "vec_attr", "val": [0, 1, 2]}], - } - } + actual = _attributes_from_json(json_attributes) + assert actual == {} - assert _ObjectAnnotation._attributes_fromdict(attributes_dict) == { - "text_attr": "text_val", - "num_attr": 0, - "bool_attr": True, - "vec_attr": [0, 1, 2], - } +def test_attributes_from_json__multiple_types( + attributes_multiple_types, attributes_multiple_types_json +): + actual = _attributes_from_json(attributes_multiple_types_json) + assert actual == attributes_multiple_types -def test_asdict__single_type(): - attributes = { - "test_text_attr0": "test_text_attr0_val", - "test_text_attr1": "test_text_attr1_val", - } - assert _ObjectAnnotation._attributes_asdict(None, attributes) == { - "text": [ - {"name": "test_text_attr0", "val": "test_text_attr0_val"}, - {"name": "test_text_attr1", "val": "test_text_attr1_val"}, - ] - } +def test_attributes_to_json__empty(): + actual = _attributes_to_json({}) + assert actual == None -def test_asdict__multiple_types(): - attributes = { - "text_attr": "text_val", - "num_attr": 0, - "bool_attr": True, - "vec_attr": [0, 1, 2], - } +def test_attributes_to_json__multiple_types( + attributes_multiple_types, attributes_multiple_types_json +): + actual = _attributes_to_json(attributes_multiple_types) + assert actual == attributes_multiple_types_json - assert _ObjectAnnotation._attributes_asdict(None, attributes) == { - "text": [{"name": "text_attr", "val": "text_val"}], - "num": [{"name": "num_attr", "val": 0}], - "boolean": [{"name": "bool_attr", "val": True}], - "vec": [{"name": "vec_attr", "val": [0, 1, 2]}], - } + +def test_attributes_to_json__unsupported_type(): + with pytest.raises(UnsupportedAttributeTypeError): + _attributes_to_json({"attribute_with_unsupported_type": object}) if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_bbox.py b/tests/test_raillabel/format/test_bbox.py index 040df09..7c061de 100644 --- a/tests/test_raillabel/format/test_bbox.py +++ b/tests/test_raillabel/format/test_bbox.py @@ -3,145 +3,69 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Bbox +from raillabel.json_format import JSONBbox # == Fixtures ========================= @pytest.fixture -def bbox_dict( - sensor_camera, - attributes_multiple_types_dict, - point2d_dict, - size2d_dict, -) -> dict: - return { - "uid": "78f0ad89-2750-4a30-9d66-44c9da73a714", - "name": "rgb_middle__bbox__person", - "val": point2d_dict + size2d_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - } - - -@pytest.fixture -def bbox( - point2d, - size2d, - sensor_camera, - attributes_multiple_types, - object_person, -) -> dict: - return Bbox( - uid="78f0ad89-2750-4a30-9d66-44c9da73a714", - pos=point2d, - size=size2d, - sensor=sensor_camera, - attributes=attributes_multiple_types, - object=object_person, +def bbox_json( + attributes_multiple_types_json, + point2d_json, + size2d_json, +) -> JSONBbox: + return JSONBbox( + uid="2811f67c-124C-4fac-a275-20807d0471de", + name="rgb_middle__bbox__person", + val=point2d_json + size2d_json, + coordinate_system="rgb_middle", + attributes=attributes_multiple_types_json, ) @pytest.fixture -def bbox_train_dict(sensor_camera, attributes_single_type_dict, point2d_dict, size2d_dict) -> dict: - return { - "uid": "6a7cfdb7-149d-4987-98dd-79d05a8cc8e6", - "name": "rgb_middle__bbox__train", - "val": point2d_dict + size2d_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_single_type_dict, - } +def bbox_id() -> UUID: + return UUID("2811f67c-124C-4fac-a275-20807d0471de") @pytest.fixture -def bbox_train( +def bbox( point2d, size2d, - sensor_camera, - attributes_single_type, - object_train, -) -> dict: + attributes_multiple_types, + object_person_id, +) -> Bbox: return Bbox( - uid="6a7cfdb7-149d-4987-98dd-79d05a8cc8e6", pos=point2d, size=size2d, - sensor=sensor_camera, - attributes=attributes_single_type, - object=object_train, + sensor_id="rgb_middle", + attributes=attributes_multiple_types, + object_id=object_person_id, ) # == Tests ============================ -def test_fromdict( - point2d, - point2d_dict, - size2d, - size2d_dict, - sensor_camera, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - bbox = Bbox.fromdict( - { - "uid": "78f0ad89-2750-4a30-9d66-44c9da73a714", - "name": "rgb_middle__bbox__person", - "val": point2d_dict + size2d_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - }, - sensors, - object_person, - ) +def test_from_json(bbox, bbox_json, object_person_id): + actual = Bbox.from_json(bbox_json, object_person_id) + assert actual == bbox - assert bbox.uid == "78f0ad89-2750-4a30-9d66-44c9da73a714" - assert bbox.name == "rgb_middle__bbox__person" - assert bbox.pos == point2d - assert bbox.size == size2d - assert bbox.object == object_person - assert bbox.sensor == sensor_camera - assert bbox.attributes == attributes_multiple_types +def test_name(bbox): + actual = bbox.name("person") + assert actual == "rgb_middle__bbox__person" -def test_asdict( - point2d, - point2d_dict, - size2d, - size2d_dict, - sensor_camera, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - bbox = Bbox( - uid="78f0ad89-2750-4a30-9d66-44c9da73a714", - pos=point2d, - size=size2d, - object=object_person, - sensor=sensor_camera, - attributes=attributes_multiple_types, - ) - assert bbox.asdict() == { - "uid": "78f0ad89-2750-4a30-9d66-44c9da73a714", - "name": "rgb_middle__bbox__person", - "val": point2d_dict + size2d_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - } +def test_to_json(bbox, bbox_json, bbox_id): + actual = bbox.to_json(bbox_id, object_type="person") + assert actual == bbox_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_camera.py b/tests/test_raillabel/format/test_camera.py new file mode 100644 index 0000000..969f4a2 --- /dev/null +++ b/tests/test_raillabel/format/test_camera.py @@ -0,0 +1,60 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from raillabel.format import Camera +from raillabel.json_format import ( + JSONCoordinateSystem, + JSONStreamCamera, + JSONStreamCameraProperties, + JSONIntrinsicsPinhole, +) + +# == Fixtures ========================= + + +@pytest.fixture +def camera_json( + intrinsics_pinhole_json, transform_json +) -> tuple[JSONStreamCamera, JSONCoordinateSystem]: + return ( + JSONStreamCamera( + type="camera", + stream_properties=JSONStreamCameraProperties(intrinsics_pinhole=intrinsics_pinhole_json), + uri="/path/to/sensor/data", + description="A very nice camera", + ), + JSONCoordinateSystem( + parent="base", type="sensor", pose_wrt_parent=transform_json, children=None + ), + ) + + +@pytest.fixture +def camera(intrinsics_pinhole, transform) -> dict: + return Camera( + intrinsics=intrinsics_pinhole, + extrinsics=transform, + uri="/path/to/sensor/data", + description="A very nice camera", + ) + + +# == Tests ============================ + + +def test_from_json(camera, camera_json): + actual = Camera.from_json(camera_json[0], camera_json[1]) + assert actual == camera + + +def test_to_json(camera, camera_json): + actual = camera.to_json() + assert actual == camera_json + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/test_raillabel/format/test_cuboid.py b/tests/test_raillabel/format/test_cuboid.py index 96af51d..b71d46f 100644 --- a/tests/test_raillabel/format/test_cuboid.py +++ b/tests/test_raillabel/format/test_cuboid.py @@ -3,116 +3,72 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Cuboid +from raillabel.json_format import JSONCuboid # == Fixtures ========================= @pytest.fixture -def cuboid_dict( - sensor_lidar, attributes_multiple_types_dict, point3d_dict, size3d_dict, quaternion_dict -) -> dict: - return { - "uid": "2c6b3de0-86c2-4684-b576-4cfd4f50d6ad", - "name": "lidar__cuboid__person", - "val": point3d_dict + quaternion_dict + size3d_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - } +def cuboid_json( + attributes_multiple_types_json, + point3d_json, + quaternion_json, + size3d_json, +) -> JSONCuboid: + return JSONCuboid( + uid="51def938-20BA-4699-95be-d6330c44cb77", + name="lidar__cuboid__person", + val=point3d_json + quaternion_json + size3d_json, + coordinate_system="lidar", + attributes=attributes_multiple_types_json, + ) + + +@pytest.fixture +def cuboid_id() -> UUID: + return UUID("51def938-20BA-4699-95be-d6330c44cb77") @pytest.fixture def cuboid( - point3d, size3d, quaternion, sensor_lidar, attributes_multiple_types, object_person -) -> dict: + point3d, + size3d, + quaternion, + attributes_multiple_types, + object_person_id, +) -> Cuboid: return Cuboid( - uid="2c6b3de0-86c2-4684-b576-4cfd4f50d6ad", pos=point3d, quat=quaternion, size=size3d, - object=object_person, - sensor=sensor_lidar, + sensor_id="lidar", attributes=attributes_multiple_types, + object_id=object_person_id, ) # == Tests ============================ -def test_fromdict( - point3d, - point3d_dict, - size3d, - size3d_dict, - quaternion, - quaternion_dict, - sensor_lidar, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - cuboid = Cuboid.fromdict( - { - "uid": "2c6b3de0-86c2-4684-b576-4cfd4f50d6ad", - "name": "lidar__cuboid__person", - "val": point3d_dict + quaternion_dict + size3d_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - }, - sensors, - object_person, - ) +def test_from_json(cuboid, cuboid_json, object_person_id): + actual = Cuboid.from_json(cuboid_json, object_person_id) + assert actual == cuboid - assert cuboid.uid == "2c6b3de0-86c2-4684-b576-4cfd4f50d6ad" - assert cuboid.name == "lidar__cuboid__person" - assert cuboid.pos == point3d - assert cuboid.quat == quaternion - assert cuboid.size == size3d - assert cuboid.object == object_person - assert cuboid.sensor == sensor_lidar - assert cuboid.attributes == attributes_multiple_types +def test_name(cuboid): + actual = cuboid.name("person") + assert actual == "lidar__cuboid__person" -def test_asdict( - point3d, - point3d_dict, - size3d, - size3d_dict, - quaternion, - quaternion_dict, - sensor_lidar, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - cuboid = Cuboid( - uid="2c6b3de0-86c2-4684-b576-4cfd4f50d6ad", - pos=point3d, - quat=quaternion, - size=size3d, - object=object_person, - sensor=sensor_lidar, - attributes=attributes_multiple_types, - ) - assert cuboid.asdict() == { - "uid": "2c6b3de0-86c2-4684-b576-4cfd4f50d6ad", - "name": "lidar__cuboid__person", - "val": point3d_dict + quaternion_dict + size3d_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - } +def test_to_json(cuboid, cuboid_json, cuboid_id): + actual = cuboid.to_json(cuboid_id, object_type="person") + assert actual == cuboid_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_element_data_pointer.py b/tests/test_raillabel/format/test_element_data_pointer.py deleted file mode 100644 index 1611ea6..0000000 --- a/tests/test_raillabel/format/test_element_data_pointer.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -import pytest - -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -from raillabel.format._attribute_type import AttributeType -from raillabel.format import ElementDataPointer - -# == Fixtures ========================= - - -@pytest.fixture -def element_data_pointer_minimal_dict() -> dict: - return { - "type": "bbox", - "frame_intervals": [], - "attribute_pointers": {}, - } - - -@pytest.fixture -def element_data_pointer_minimal(): - return ElementDataPointer( - uid="rgb_middle__bbox__person", frame_intervals=[], attribute_pointers={} - ) - - -@pytest.fixture -def element_data_pointer_full_dict(frame_interval_dict) -> dict: - return { - "type": "bbox", - "frame_intervals": [frame_interval_dict], - "attribute_pointers": { - "text_attr": "text", - "num_attr": "num", - "bool_attr": "boolean", - "vec_attr": "vec", - }, - } - - -@pytest.fixture -def element_data_pointer_full(sensor_camera, object_person, frame_interval): - return ElementDataPointer( - uid="rgb_middle__bbox__person", - frame_intervals=[frame_interval], - attribute_pointers={ - "text_attr": AttributeType.TEXT, - "num_attr": AttributeType.NUM, - "bool_attr": AttributeType.BOOLEAN, - "vec_attr": AttributeType.VEC, - }, - ) - - -# == Tests ============================ - - -def test_asdict_minimal(sensor_camera, object_person): - element_data_pointer = ElementDataPointer( - uid="rgb_middle__bbox__person", frame_intervals=[], attribute_pointers={} - ) - - assert element_data_pointer.asdict() == { - "type": "bbox", - "frame_intervals": [], - "attribute_pointers": {}, - } - - -def test_asdict_full(sensor_camera, object_person, frame_interval, frame_interval_dict): - element_data_pointer = ElementDataPointer( - uid="rgb_middle__bbox__person", - frame_intervals=[frame_interval], - attribute_pointers={ - "text_attr": AttributeType.TEXT, - "num_attr": AttributeType.NUM, - "bool_attr": AttributeType.BOOLEAN, - "vec_attr": AttributeType.VEC, - }, - ) - - assert element_data_pointer.asdict() == { - "type": "bbox", - "frame_intervals": [frame_interval_dict], - "attribute_pointers": { - "text_attr": "text", - "num_attr": "num", - "bool_attr": "boolean", - "vec_attr": "vec", - }, - } - - -if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_frame.py b/tests/test_raillabel/format/test_frame.py index c644794..1b85ba6 100644 --- a/tests/test_raillabel/format/test_frame.py +++ b/tests/test_raillabel/format/test_frame.py @@ -3,166 +3,105 @@ from __future__ import annotations -import os -import sys from decimal import Decimal -from pathlib import Path import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Frame +from raillabel.json_format import ( + JSONFrame, + JSONFrameData, + JSONFrameProperties, + JSONObjectData, + JSONAnnotations, +) # == Fixtures ========================= @pytest.fixture -def frame_dict( - sensor_reference_camera_dict, - num_dict, - object_person, - object_data_person_dict, - object_train, - object_data_train_dict, -) -> dict: - return { - "frame_properties": { - "timestamp": "1632321743.100000072", - "streams": {"rgb_middle": sensor_reference_camera_dict}, - "frame_data": {"num": [num_dict]}, - }, - "objects": { - object_person.uid: object_data_person_dict, - object_train.uid: object_data_train_dict, +def frame_json( + sensor_reference_json, + another_sensor_reference_json, + num_json, + bbox_json, + cuboid_json, + poly2d_json, + poly3d_json, + seg3d_json, +) -> JSONFrame: + return JSONFrame( + frame_properties=JSONFrameProperties( + timestamp=Decimal("1631337747.123123123"), + streams={ + "rgb_middle": sensor_reference_json, + "lidar": another_sensor_reference_json, + }, + frame_data=JSONFrameData(num=[num_json]), + ), + objects={ + "cfcf9750-3bc3-4077-9079-a82c0c63976a": JSONObjectData( + object_data=JSONAnnotations( + poly2d=[poly2d_json], + poly3d=[poly3d_json], + ) + ), + "b40ba3ad-0327-46ff-9c28-2506cfd6d934": JSONObjectData( + object_data=JSONAnnotations( + bbox=[bbox_json], + cuboid=[cuboid_json], + vec=[seg3d_json], + ) + ), }, - } + ) @pytest.fixture -def frame(sensor_reference_camera, num, all_annotations) -> dict: +def frame( + sensor_reference, + another_sensor_reference, + num, + bbox, + bbox_id, + cuboid, + cuboid_id, + poly2d, + poly2d_id, + poly3d, + poly3d_id, + seg3d, + seg3d_id, +) -> dict: return Frame( - timestamp=Decimal("1632321743.100000072"), - sensors={sensor_reference_camera.sensor.uid: sensor_reference_camera}, - frame_data={num.name: num}, - annotations=all_annotations, - ) - - -# == Tests ============================ - - -def test_fromdict_sensors(sensor_reference_camera_dict, sensor_reference_camera, sensor_camera): - frame = Frame.fromdict( - data_dict={ - "frame_properties": { - "timestamp": "1632321743.100000072", - "streams": {"rgb_middle": sensor_reference_camera_dict}, - } - }, - sensors={sensor_camera.uid: sensor_camera}, - objects={}, - ) - - assert frame.timestamp == Decimal("1632321743.100000072") - assert frame.sensors == {sensor_reference_camera.sensor.uid: sensor_reference_camera} - - -def test_fromdict_frame_data(num, num_dict, sensor_camera): - frame = Frame.fromdict( - data_dict={"frame_properties": {"frame_data": {"num": [num_dict]}}}, - sensors={sensor_camera.uid: sensor_camera}, - objects={}, - ) - - assert frame.frame_data == {num.name: num} - - -def test_fromdict_annotations( - object_data_person_dict, - object_person, - object_data_train_dict, - object_train, - sensors, - all_annotations, -): - frame = Frame.fromdict( - data_dict={ - "objects": { - object_person.uid: object_data_person_dict, - object_train.uid: object_data_train_dict, - } + timestamp=Decimal("1631337747.123123123"), + sensors={ + "rgb_middle": sensor_reference, + "lidar": another_sensor_reference, }, - sensors=sensors, - objects={ - object_person.uid: object_person, - object_train.uid: object_train, + frame_data={num.name: num}, + annotations={ + bbox_id: bbox, + cuboid_id: cuboid, + poly2d_id: poly2d, + poly3d_id: poly3d, + seg3d_id: seg3d, }, ) - assert frame.annotations == all_annotations - - -def test_asdict_sensors( - sensor_reference_camera_dict, - sensor_reference_camera, -): - frame = Frame( - timestamp=Decimal("1632321743.100000072"), - sensors={sensor_reference_camera.sensor.uid: sensor_reference_camera}, - ) - - assert frame.asdict() == { - "frame_properties": { - "timestamp": "1632321743.100000072", - "streams": {"rgb_middle": sensor_reference_camera_dict}, - } - } - - -def test_asdict_frame_data(num, num_dict): - frame = Frame(frame_data={num.name: num}) - - assert frame.asdict() == {"frame_properties": {"frame_data": {"num": [num_dict]}}} +# == Tests ============================ -def test_asdict_object_data( - object_data_person_dict, object_person, object_data_train_dict, object_train, all_annotations -): - frame = Frame(annotations=all_annotations) - assert frame.asdict() == { - "objects": { - object_person.uid: object_data_person_dict, - object_train.uid: object_data_train_dict, - } - } +def test_from_json(frame, frame_json): + actual = Frame.from_json(frame_json) + assert actual == frame -def test_object_data(object_person, object_train, bbox, cuboid, poly2d, poly3d, seg3d, bbox_train): - frame = Frame( - annotations={ - bbox.uid: bbox, - poly2d.uid: poly2d, - cuboid.uid: cuboid, - poly3d.uid: poly3d, - seg3d.uid: seg3d, - bbox_train.uid: bbox_train, - }, - ) - - assert frame.object_data == { - object_person.uid: { - bbox.uid: bbox, - poly2d.uid: poly2d, - cuboid.uid: cuboid, - poly3d.uid: poly3d, - seg3d.uid: seg3d, - }, - object_train.uid: {bbox_train.uid: bbox_train}, - } +def test_to_json(frame, frame_json, objects): + actual = frame.to_json(objects) + assert actual == frame_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-vv"]) + pytest.main([__file__, "-vv"]) diff --git a/tests/test_raillabel/format/test_frame_interval.py b/tests/test_raillabel/format/test_frame_interval.py index 86ad148..7633ec7 100644 --- a/tests/test_raillabel/format/test_frame_interval.py +++ b/tests/test_raillabel/format/test_frame_interval.py @@ -11,11 +11,6 @@ # == Fixtures ========================= -@pytest.fixture -def frame_interval_dict() -> dict: - return {"frame_start": 12, "frame_end": 16} - - @pytest.fixture def frame_interval_json() -> JSONFrameInterval: return JSONFrameInterval(frame_start=12, frame_end=16) @@ -37,30 +32,6 @@ def test_from_json(frame_interval, frame_interval_json): assert actual == frame_interval -def test_fromdict(): - frame_interval = FrameInterval.fromdict( - { - "frame_start": 12, - "frame_end": 16, - } - ) - - assert frame_interval.start == 12 - assert frame_interval.end == 16 - - -def test_asdict(): - frame_interval = FrameInterval( - start=12, - end=16, - ) - - assert frame_interval.asdict() == { - "frame_start": 12, - "frame_end": 16, - } - - def test_len(): frame_interval = FrameInterval( start=12, @@ -70,28 +41,28 @@ def test_len(): assert len(frame_interval) == 5 -def test_from_frame_uids_empty(): - frame_uids = [] +def test_from_frame_ids_empty(): + frame_ids = [] - assert FrameInterval.from_frame_uids(frame_uids) == [] + assert FrameInterval.from_frame_ids(frame_ids) == [] -def test_from_frame_uids_one_frame(): - frame_uids = [1] +def test_from_frame_ids_one_frame(): + frame_ids = [1] - assert FrameInterval.from_frame_uids(frame_uids) == [FrameInterval(1, 1)] + assert FrameInterval.from_frame_ids(frame_ids) == [FrameInterval(1, 1)] -def test_from_frame_uids_one_interval(): - frame_uids = [1, 2, 3, 4] +def test_from_frame_ids_one_interval(): + frame_ids = [1, 2, 3, 4] - assert FrameInterval.from_frame_uids(frame_uids) == [FrameInterval(1, 4)] + assert FrameInterval.from_frame_ids(frame_ids) == [FrameInterval(1, 4)] -def test_from_frame_uids_multiple_intervals(): - frame_uids = [0, 1, 2, 3, 6, 7, 9, 12, 13, 14] +def test_from_frame_ids_multiple_intervals(): + frame_ids = [0, 1, 2, 3, 6, 7, 9, 12, 13, 14] - assert FrameInterval.from_frame_uids(frame_uids) == [ + assert FrameInterval.from_frame_ids(frame_ids) == [ FrameInterval(0, 3), FrameInterval(6, 7), FrameInterval(9, 9), @@ -99,14 +70,19 @@ def test_from_frame_uids_multiple_intervals(): ] -def test_from_frame_uids_unsorted(): - frame_uids = [5, 2, 1, 3] +def test_from_frame_ids_unsorted(): + frame_ids = [5, 2, 1, 3] - assert FrameInterval.from_frame_uids(frame_uids) == [ + assert FrameInterval.from_frame_ids(frame_ids) == [ FrameInterval(1, 3), FrameInterval(5, 5), ] +def test_to_json(frame_interval, frame_interval_json): + actual = frame_interval.to_json() + assert actual == frame_interval_json + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_gps_imu.py b/tests/test_raillabel/format/test_gps_imu.py new file mode 100644 index 0000000..6bc33e1 --- /dev/null +++ b/tests/test_raillabel/format/test_gps_imu.py @@ -0,0 +1,51 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from raillabel.format import GpsImu +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +# == Fixtures ========================= + + +@pytest.fixture +def gps_imu_json(transform_json) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + return ( + JSONStreamOther( + type="gps_imu", + uri="/path/to/sensor/data", + description="A very nice gps_imu", + ), + JSONCoordinateSystem( + parent="base", type="sensor", pose_wrt_parent=transform_json, children=None + ), + ) + + +@pytest.fixture +def gps_imu(transform) -> dict: + return GpsImu( + extrinsics=transform, + uri="/path/to/sensor/data", + description="A very nice gps_imu", + ) + + +# == Tests ============================ + + +def test_from_json(gps_imu, gps_imu_json): + actual = GpsImu.from_json(gps_imu_json[0], gps_imu_json[1]) + assert actual == gps_imu + + +def test_to_json(gps_imu, gps_imu_json): + actual = gps_imu.to_json() + assert actual == gps_imu_json + + +if __name__ == "__main__": + pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_intrinsics_pinhole.py b/tests/test_raillabel/format/test_intrinsics_pinhole.py index fb41360..521a6e6 100644 --- a/tests/test_raillabel/format/test_intrinsics_pinhole.py +++ b/tests/test_raillabel/format/test_intrinsics_pinhole.py @@ -11,16 +11,6 @@ # == Fixtures ========================= -@pytest.fixture -def intrinsics_pinhole_dict() -> dict: - return { - "camera_matrix": [0.48, 0, 0.81, 0, 0, 0.16, 0.83, 0, 0, 0, 1, 0], - "distortion_coeffs": [0.49, 0.69, 0.31, 0.81, 0.99], - "width_px": 2464, - "height_px": 1600, - } - - @pytest.fixture def intrinsics_pinhole_json() -> dict: return JSONIntrinsicsPinhole( @@ -49,36 +39,9 @@ def test_from_json(intrinsics_pinhole, intrinsics_pinhole_json): assert actual == intrinsics_pinhole -def test_fromdict(): - intrinsics_pinhole = IntrinsicsPinhole.fromdict( - { - "camera_matrix": [0.48, 0, 0.81, 0, 0, 0.16, 0.83, 0, 0, 0, 1, 0], - "distortion_coeffs": [0.49, 0.69, 0.31, 0.81, 0.99], - "width_px": 2464, - "height_px": 1600, - } - ) - - assert intrinsics_pinhole.camera_matrix == (0.48, 0, 0.81, 0, 0, 0.16, 0.83, 0, 0, 0, 1, 0) - assert intrinsics_pinhole.distortion == (0.49, 0.69, 0.31, 0.81, 0.99) - assert intrinsics_pinhole.width_px == 2464 - assert intrinsics_pinhole.height_px == 1600 - - -def test_asdict(): - intrinsics_pinhole = IntrinsicsPinhole( - camera_matrix=(0.48, 0, 0.81, 0, 0, 0.16, 0.83, 0, 0, 0, 1, 0), - distortion=(0.49, 0.69, 0.31, 0.81, 0.99), - width_px=2464, - height_px=1600, - ) - - assert intrinsics_pinhole.asdict() == { - "camera_matrix": [0.48, 0, 0.81, 0, 0, 0.16, 0.83, 0, 0, 0, 1, 0], - "distortion_coeffs": [0.49, 0.69, 0.31, 0.81, 0.99], - "width_px": 2464, - "height_px": 1600, - } +def test_to_json(intrinsics_pinhole, intrinsics_pinhole_json): + actual = intrinsics_pinhole.to_json() + assert actual == intrinsics_pinhole_json if __name__ == "__main__": diff --git a/tests/test_raillabel/format/test_intrinsics_radar.py b/tests/test_raillabel/format/test_intrinsics_radar.py index d796a15..b0043b9 100644 --- a/tests/test_raillabel/format/test_intrinsics_radar.py +++ b/tests/test_raillabel/format/test_intrinsics_radar.py @@ -11,15 +11,6 @@ # == Fixtures ========================= -@pytest.fixture -def intrinsics_radar_dict() -> dict: - return { - "resolution_px_per_m": 2.856, - "width_px": 2856, - "height_px": 1428, - } - - @pytest.fixture def intrinsics_radar_json() -> dict: return JSONIntrinsicsRadar( @@ -46,32 +37,9 @@ def test_from_json(intrinsics_radar, intrinsics_radar_json): assert actual == intrinsics_radar -def test_fromdict(): - intrinsics_radar = IntrinsicsRadar.fromdict( - { - "resolution_px_per_m": 2.856, - "width_px": 2856, - "height_px": 1428, - } - ) - - assert intrinsics_radar.resolution_px_per_m == 2.856 - assert intrinsics_radar.width_px == 2856 - assert intrinsics_radar.height_px == 1428 - - -def test_asdict(): - intrinsics_radar = IntrinsicsRadar( - resolution_px_per_m=2.856, - width_px=2856, - height_px=1428, - ) - - assert intrinsics_radar.asdict() == { - "resolution_px_per_m": 2.856, - "width_px": 2856, - "height_px": 1428, - } +def test_to_json(intrinsics_radar, intrinsics_radar_json): + actual = intrinsics_radar.to_json() + assert actual == intrinsics_radar_json if __name__ == "__main__": diff --git a/tests/test_raillabel/format/test_lidar.py b/tests/test_raillabel/format/test_lidar.py new file mode 100644 index 0000000..a4d151d --- /dev/null +++ b/tests/test_raillabel/format/test_lidar.py @@ -0,0 +1,51 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from raillabel.format import Lidar +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +# == Fixtures ========================= + + +@pytest.fixture +def lidar_json(transform_json) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + return ( + JSONStreamOther( + type="lidar", + uri="/path/to/sensor/data", + description="A very nice lidar", + ), + JSONCoordinateSystem( + parent="base", type="sensor", pose_wrt_parent=transform_json, children=None + ), + ) + + +@pytest.fixture +def lidar(transform) -> dict: + return Lidar( + extrinsics=transform, + uri="/path/to/sensor/data", + description="A very nice lidar", + ) + + +# == Tests ============================ + + +def test_from_json(lidar, lidar_json): + actual = Lidar.from_json(lidar_json[0], lidar_json[1]) + assert actual == lidar + + +def test_to_json(lidar, lidar_json): + actual = lidar.to_json() + assert actual == lidar_json + + +if __name__ == "__main__": + pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_metadata.py b/tests/test_raillabel/format/test_metadata.py index 5634297..6ba5379 100644 --- a/tests/test_raillabel/format/test_metadata.py +++ b/tests/test_raillabel/format/test_metadata.py @@ -3,127 +3,68 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Metadata +from raillabel.json_format import JSONMetadata # == Fixtures ========================= @pytest.fixture -def metadata_minimal_dict() -> dict: - return {"schema_version": "1.0.0"} - - -@pytest.fixture -def metadata_full_dict() -> dict: - return { - "schema_version": "1.0.0", - "annotator": "test_annotator", - "subschema_version": "2.1.0", - "comment": "test_comment", - "name": "test_project", - "tagged_file": "test_folder", - } - - -@pytest.fixture -def metadata_minimal() -> dict: - return Metadata(schema_version="1.0.0") +def metadata_json() -> JSONMetadata: + return JSONMetadata( + schema_version="1.0.0", + name="some_file", + subschema_version="4.0.0", + exporter_version="1.2.3", + file_version="0.1.5", + tagged_file="path/to/data", + annotator="John Doe", + comment="this is a very nice annotation file", + ) @pytest.fixture -def metadata_full() -> dict: +def metadata() -> dict: return Metadata( schema_version="1.0.0", - annotator="test_annotator", - subschema_version="2.1.0", - comment="test_comment", - name="test_project", - tagged_file="test_folder", + name="some_file", + subschema_version="4.0.0", + exporter_version="1.2.3", + file_version="0.1.5", + tagged_file="path/to/data", + annotator="John Doe", + comment="this is a very nice annotation file", ) # == Tests ============================ -def test_fromdict_minimal(): - metadata = Metadata.fromdict( - {"schema_version": "1.0.0"}, - ) - - assert metadata.schema_version == "1.0.0" - assert metadata.annotator is None +def test_from_json(metadata, metadata_json): + actual = Metadata.from_json(metadata_json) + assert actual == metadata -def test_fromdict_full(): - metadata = Metadata.fromdict( - { +def test_from_json__extra_fields(): + json_metadata = JSONMetadata( + **{ "schema_version": "1.0.0", - "annotator": "test_annotator", - "subschema_version": "2.1.0", - "comment": "test_comment", - "name": "test_project", - "tagged_file": "test_folder", - }, - "2.1.1", + "ADDITIONAL_STR": "SOME_VALUE", + "ADDITIONAL_OBJECT": {"first_field": 2, "second_field": [1, 2, 3]}, + } ) - assert metadata.annotator == "test_annotator" - assert metadata.schema_version == "1.0.0" - assert metadata.comment == "test_comment" - assert metadata.name == "test_project" - assert metadata.subschema_version == "2.1.1" - assert metadata.tagged_file == "test_folder" - - -def test_fromdict_additional_arg_valid(): - metadata = Metadata.fromdict({"schema_version": "1.0.0", "additional_argument": "Some Value"}) - - assert metadata.schema_version == "1.0.0" - assert metadata.additional_argument == "Some Value" - - -def test_asdict_minimal(): - metadata_dict = Metadata(schema_version="1.0.0").asdict() - - assert metadata_dict == {"schema_version": "1.0.0"} - - -def test_asdict_full(): - metadata_dict = Metadata( - annotator="test_annotator", - schema_version="1.0.0", - comment="test_comment", - name="test_project", - subschema_version="2.1.0", - tagged_file="test_folder", - ).asdict() - - assert metadata_dict == { - "schema_version": "1.0.0", - "annotator": "test_annotator", - "subschema_version": "2.1.0", - "comment": "test_comment", - "name": "test_project", - "tagged_file": "test_folder", - } - - -def test_fromdict_additional_arg(): - metadata = Metadata(schema_version="1.0.0") + actual = Metadata.from_json(json_metadata) + assert actual.ADDITIONAL_STR == "SOME_VALUE" + assert actual.ADDITIONAL_OBJECT == {"first_field": 2, "second_field": [1, 2, 3]} - metadata.additional_argument = "Some Value" - assert metadata.asdict() == {"schema_version": "1.0.0", "additional_argument": "Some Value"} +def test_to_json(metadata, metadata_json): + actual = metadata.to_json() + assert actual == metadata_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_num.py b/tests/test_raillabel/format/test_num.py index 7c3d0c1..2779c86 100644 --- a/tests/test_raillabel/format/test_num.py +++ b/tests/test_raillabel/format/test_num.py @@ -3,75 +3,53 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Num +from raillabel.json_format import JSONNum # == Fixtures ========================= @pytest.fixture -def num_dict(sensor_camera) -> dict: - return { - "uid": "4e86c449-3B19-410c-aa64-603d46da3b26", - "name": "some_number", - "val": 24, - "coordinate_system": sensor_camera.uid, - } +def num_json(num_id) -> JSONNum: + return JSONNum( + uid=num_id, + name="velocity", + val=49.21321, + coordinate_system="gps_imu", + ) + + +@pytest.fixture +def num_id() -> UUID: + return UUID("78f0ad89-2750-4a30-9d66-44c9da73a714") @pytest.fixture -def num(sensor_camera) -> dict: +def num(num_id) -> Num: return Num( - uid="4e86c449-3B19-410c-aa64-603d46da3b26", - name="some_number", - val=24, - sensor=sensor_camera, + sensor_id="gps_imu", + name="velocity", + val=49.21321, + id=num_id, ) # == Tests ============================ -def test_fromdict(sensor_camera): - num = Num.fromdict( - { - "uid": "4e86c449-3B19-410c-aa64-603d46da3b26", - "name": "some_number", - "val": 24, - "coordinate_system": sensor_camera.uid, - }, - {sensor_camera.uid: sensor_camera}, - ) - - assert num.uid == "4e86c449-3B19-410c-aa64-603d46da3b26" - assert num.name == "some_number" - assert num.val == 24 - assert num.sensor == sensor_camera +def test_from_json(num, num_json): + actual = Num.from_json(num_json) + assert actual == num -def test_asdict(sensor_camera): - num = Num( - uid="4e86c449-3B19-410c-aa64-603d46da3b26", - name="some_number", - val=24, - sensor=sensor_camera, - ) - - assert num.asdict() == { - "uid": "4e86c449-3B19-410c-aa64-603d46da3b26", - "name": "some_number", - "val": 24, - "coordinate_system": sensor_camera.uid, - } +def test_to_json(num, num_json): + actual = num.to_json() + assert actual == num_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_object.py b/tests/test_raillabel/format/test_object.py index 482f49a..a7b69a1 100644 --- a/tests/test_raillabel/format/test_object.py +++ b/tests/test_raillabel/format/test_object.py @@ -3,421 +3,149 @@ from __future__ import annotations -import os -import random -import sys -import typing as t -from pathlib import Path -from uuid import uuid4 +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -from raillabel.format._attribute_type import AttributeType -from raillabel.format import ( - Bbox, - Cuboid, - Frame, - FrameInterval, - Object, - Point2d, - Point3d, - Quaternion, - Sensor, - Size2d, - Size3d, -) +from raillabel.json_format import JSONObject, JSONFrameInterval, JSONElementDataPointer +from raillabel.format import Object # == Fixtures ========================= @pytest.fixture -def objects_dict(object_person_dict, object_train_dict) -> dict: +def objects(object_person, object_person_id, object_track, object_track_id) -> dict[UUID, Object]: return { - object_person_dict["object_uid"]: object_person_dict["data_dict"], - object_train_dict["object_uid"]: object_train_dict["data_dict"], + object_person_id: object_person, + object_track_id: object_track, } @pytest.fixture -def objects(object_person, object_train) -> dict[str, Object]: - return { - object_person.uid: object_person, - object_train.uid: object_train, - } - - -@pytest.fixture -def object_person_dict() -> dict: - return { - "object_uid": "b40ba3ad-0327-46ff-9c28-2506cfd6d934", - "data_dict": { - "name": "person_0000", - "type": "person", +def object_person_json() -> JSONObject: + return JSONObject( + name="person_0032", + type="person", + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + object_data_pointers={ + "rgb_middle__bbox__person": JSONElementDataPointer( + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + type="bbox", + attribute_pointers={ + "has_red_hat": "boolean", + "has_green_hat": "boolean", + "number_of_red_clothing_items": "num", + "color_of_hat": "text", + "clothing_items": "vec", + }, + ), + "lidar__cuboid__person": JSONElementDataPointer( + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + type="cuboid", + attribute_pointers={ + "has_red_hat": "boolean", + "has_green_hat": "boolean", + "number_of_red_clothing_items": "num", + "color_of_hat": "text", + "clothing_items": "vec", + }, + ), + "lidar__vec__person": JSONElementDataPointer( + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + type="vec", + attribute_pointers={ + "has_red_hat": "boolean", + "has_green_hat": "boolean", + "number_of_red_clothing_items": "num", + "color_of_hat": "text", + "clothing_items": "vec", + }, + ), }, - } + ) @pytest.fixture -def object_person() -> dict: +def object_person() -> Object: return Object( - uid="b40ba3ad-0327-46ff-9c28-2506cfd6d934", - name="person_0000", + name="person_0032", type="person", ) @pytest.fixture -def object_train_dict() -> dict: - return { - "object_uid": "d51a19be-8bc2-4a82-b66a-03c8de95b0cf", - "data_dict": { - "name": "train_0000", - "type": "train", - }, - } +def object_person_id() -> UUID: + return UUID("b40ba3ad-0327-46ff-9c28-2506cfd6d934") @pytest.fixture -def object_train() -> dict: - return Object( - uid="d51a19be-8bc2-4a82-b66a-03c8de95b0cf", - name="train_0000", - type="train", - ) - - -# == Tests ============================ - - -def test_fromdict(): - object = Object.fromdict( - object_uid="b40ba3ad-0327-46ff-9c28-2506cfd6d934", - data_dict={ - "name": "person_0000", - "type": "person", +def object_track_json() -> JSONObject: + return JSONObject( + name="track_0001", + type="track", + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + object_data_pointers={ + "rgb_middle__poly2d__track": JSONElementDataPointer( + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + type="poly2d", + attribute_pointers={ + "has_red_hat": "boolean", + "has_green_hat": "boolean", + "number_of_red_clothing_items": "num", + "color_of_hat": "text", + "clothing_items": "vec", + }, + ), + "lidar__poly3d__track": JSONElementDataPointer( + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + type="poly3d", + attribute_pointers={ + "has_red_hat": "boolean", + "has_green_hat": "boolean", + "number_of_red_clothing_items": "num", + "color_of_hat": "text", + "clothing_items": "vec", + }, + ), }, ) - assert object.uid == "b40ba3ad-0327-46ff-9c28-2506cfd6d934" - assert object.name == "person_0000" - assert object.type == "person" - - -def test_asdict_no_frames(): - object = Object( - uid="b40ba3ad-0327-46ff-9c28-2506cfd6d934", - name="person_0000", - type="person", - ) - - assert object.asdict() == { - "name": "person_0000", - "type": "person", - } - - -def test_asdict_with_frames(): - object = Object( - uid="b40ba3ad-0327-46ff-9c28-2506cfd6d934", - name="person_0000", - type="person", - ) - - frames = { - 0: build_frame(0, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - } - - object_dict = object.asdict(frames) - - assert "frame_intervals" in object_dict - assert "object_data_pointers" in object_dict - assert "rgb_middle__bbox__person" in object_dict["object_data_pointers"] - - -def test_frame_intervals(): - object = Object( - uid="b40ba3ad-0327-46ff-9c28-2506cfd6d934", - name="person_0000", - type="person", - ) - - frames = { - 0: build_frame(0, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - 1: build_frame(1, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - 2: build_frame(2, {}), - 3: build_frame(3, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - } - - assert object.frame_intervals(frames) == [ - FrameInterval(0, 1), - FrameInterval(3, 3), - ] - - -def test_object_data_pointers__sensor(): - object = build_object("person") - - frames = { - 0: build_frame( - 0, - { - object: [ - build_annotation("rgb_middle__bbox__person", object), - build_annotation("lidar__bbox__person", object), - ] - }, - ) - } - - object_data_pointers = object.object_data_pointers(frames) - - assert set(object_data_pointers.keys()) == set( - ["rgb_middle__bbox__person", "lidar__bbox__person"] - ) - -def test_object_data_pointers__annotation_type(): - object = build_object("person") - - frames = { - 0: build_frame( - 0, - { - object: [ - build_annotation("rgb_middle__bbox__person", object), - build_annotation("rgb_middle__cuboid__person", object), - ] - }, - ) - } - - object_data_pointers = object.object_data_pointers(frames) - - assert set(object_data_pointers.keys()) == set( - ["rgb_middle__bbox__person", "rgb_middle__cuboid__person"] +@pytest.fixture +def object_track() -> Object: + return Object( + name="track_0001", + type="track", ) -def test_object_data_pointers__one_frame_interval(): - object = build_object("person") - - frames = { - 0: build_frame(0, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - 1: build_frame(1, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - } - - object_data_pointers = object.object_data_pointers(frames) - - assert len(object_data_pointers) == 1 - assert object_data_pointers["rgb_middle__bbox__person"].frame_intervals == [FrameInterval(0, 1)] - - -def test_object_data_pointers__two_frame_intervals(): - object = build_object("person") - - frames = { - 0: build_frame(0, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - 1: build_frame(1, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - 8: build_frame(8, {object: [build_annotation("rgb_middle__bbox__person", object)]}), - } - - object_data_pointers = object.object_data_pointers(frames) - - assert len(object_data_pointers) == 1 - assert object_data_pointers["rgb_middle__bbox__person"].frame_intervals == [ - FrameInterval(0, 1), - FrameInterval(8, 8), - ] - - -def test_object_data_pointers__attributes_one_annotation(): - object = build_object("person") - - frames = { - 0: build_frame( - 0, - { - object: [ - build_annotation( - name="rgb_middle__bbox__person", - object=object, - attributes={ - "text_attr": "some value", - "num_attr": 0, - "bool_attr": True, - "vec_attr": [0, 1], - }, - ), - ] - }, - ) - } - - object_data_pointers = object.object_data_pointers(frames) - - assert object_data_pointers["rgb_middle__bbox__person"].attribute_pointers == { - "text_attr": AttributeType.TEXT, - "num_attr": AttributeType.NUM, - "bool_attr": AttributeType.BOOLEAN, - "vec_attr": AttributeType.VEC, - } - - -def test_object_data_pointers__attributes_multiple_annotations_with_differing_attributes(): - object = build_object("person") - - frames = { - 0: build_frame( - 0, - { - object: [ - build_annotation( - name="rgb_middle__bbox__person", - object=object, - attributes={ - "text_attr": "some value", - "num_attr": 0, - }, - ), - build_annotation( - name="rgb_middle__bbox__person", - object=object, - attributes={ - "bool_attr": True, - "vec_attr": [0, 1], - }, - ), - ] - }, - ) - } - - object_data_pointers = object.object_data_pointers(frames) - - assert object_data_pointers["rgb_middle__bbox__person"].attribute_pointers == { - "text_attr": AttributeType.TEXT, - "num_attr": AttributeType.NUM, - "bool_attr": AttributeType.BOOLEAN, - "vec_attr": AttributeType.VEC, - } - - -def test_object_data_pointers__multiple_objects_of_differing_type(): - object_person = build_object("person") - object_train = build_object("train") - - frames = { - 0: build_frame( - 0, - { - object_person: [ - build_annotation("lidar__bbox__person", object_person), - ] - }, - ), - 1: build_frame( - 1, - { - object_train: [ - build_annotation("lidar__bbox__train", object_train), - ] - }, - ), - } - - person_object_data_pointers = object_person.object_data_pointers(frames) - assert len(person_object_data_pointers) == 1 - assert person_object_data_pointers["lidar__bbox__person"].frame_intervals == [ - FrameInterval(0, 0) - ] - - train_object_data_pointers = object_train.object_data_pointers(frames) - assert len(train_object_data_pointers) == 1 - assert train_object_data_pointers["lidar__bbox__train"].frame_intervals == [FrameInterval(1, 1)] - - -def test_object_data_pointers__multiple_objects_of_same_type(): - object1 = build_object("person") - object2 = build_object("person") - - frames = { - 0: build_frame( - 0, - { - object1: [ - build_annotation("lidar__bbox__person", object1), - ] - }, - ), - 1: build_frame( - 1, - { - object2: [ - build_annotation("lidar__bbox__person", object2), - ] - }, - ), - } - - object1_data_pointers = object1.object_data_pointers(frames) - assert len(object1_data_pointers) == 1 - assert object1_data_pointers["lidar__bbox__person"].frame_intervals == [FrameInterval(0, 0)] - - object2_data_pointers = object2.object_data_pointers(frames) - assert len(object2_data_pointers) == 1 - assert object2_data_pointers["lidar__bbox__person"].frame_intervals == [FrameInterval(1, 1)] - - -# == Helpers ========================== - +@pytest.fixture +def object_track_id() -> UUID: + return UUID("cfcf9750-3BC3-4077-9079-a82c0c63976a") -def build_annotation(name: str, object: Object, attributes: dict = {}) -> t.Union[Bbox, Cuboid]: - sensor_uid, ann_type, object_type = tuple(name.split("__")) - sensor = Sensor(sensor_uid) +# == Tests ============================ - if ann_type == "bbox": - return Bbox( - uid=str(uuid4()), - object=object, - attributes=attributes, - sensor=sensor, - pos=Point2d(50, 100), - size=Size2d(30, 30), - ) - elif ann_type == "cuboid": - return Cuboid( - uid=str(uuid4()), - object=object, - attributes=attributes, - sensor=sensor, - pos=Point3d(50, 100, 20), - size=Size3d(30, 30, 30), - quat=Quaternion(0, 0, 0, 1), - ) +def test_from_json__person(object_person, object_person_json): + actual = Object.from_json(object_person_json) + assert actual == object_person - else: - raise ValueError() +def test_from_json__track(object_track, object_track_json): + actual = Object.from_json(object_track_json) + assert actual == object_track -def build_frame(uid: int, raw_object_data: dict[Object, list[t.Union[Bbox, Cuboid]]]) -> Frame: - annotations = {} - for object_data in raw_object_data.values(): - for annotation in object_data: - annotations[annotation.uid] = annotation - return Frame(annotations=annotations) +def test_to_json__person(object_person, object_person_json, object_person_id, frame): + actual = object_person.to_json(object_person_id, {1: frame}) + assert actual == object_person_json -def build_object(type: str) -> Object: - return Object( - uid=str(uuid4()), name=f"{type}_{str(random.randint(0, 9999)).zfill(4)}", type=type - ) +def test_to_json__track(object_track, object_track_json, object_track_id, frame): + actual = object_track.to_json(object_track_id, {1: frame}) + assert actual == object_track_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-vv"]) diff --git a/tests/test_raillabel/format/test_object_annotation.py b/tests/test_raillabel/format/test_object_annotation.py deleted file mode 100644 index 143ca47..0000000 --- a/tests/test_raillabel/format/test_object_annotation.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -import pytest - -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -import raillabel -from raillabel.format import annotation_classes - -# == Fixtures ========================= - - -@pytest.fixture -def all_annotations( - bbox, - bbox_train, - cuboid, - poly2d, - poly3d, - seg3d, -): - return { - bbox.uid: bbox, - bbox_train.uid: bbox_train, - cuboid.uid: cuboid, - poly2d.uid: poly2d, - poly3d.uid: poly3d, - seg3d.uid: seg3d, - } - - -# == Tests ============================ - - -def test_annotation_classes(): - assert annotation_classes() == { - "bbox": raillabel.format.Bbox, - "poly2d": raillabel.format.Poly2d, - "cuboid": raillabel.format.Cuboid, - "poly3d": raillabel.format.Poly3d, - "vec": raillabel.format.Seg3d, - } - - -# Executes the test if the file is called -if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_object_data.py b/tests/test_raillabel/format/test_object_data.py deleted file mode 100644 index 244f930..0000000 --- a/tests/test_raillabel/format/test_object_data.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -import pytest - -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -# == Fixtures ========================= - - -@pytest.fixture -def object_data_person_dict(bbox_dict, poly2d_dict, cuboid_dict, poly3d_dict, seg3d_dict) -> dict: - return { - "object_data": { - "bbox": [bbox_dict], - "poly2d": [poly2d_dict], - "cuboid": [cuboid_dict], - "poly3d": [poly3d_dict], - "vec": [seg3d_dict], - } - } - - -@pytest.fixture -def object_data_train_dict(bbox_train_dict) -> dict: - return { - "object_data": { - "bbox": [bbox_train_dict], - } - } - - -if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_other_sensor.py b/tests/test_raillabel/format/test_other_sensor.py new file mode 100644 index 0000000..2cb693a --- /dev/null +++ b/tests/test_raillabel/format/test_other_sensor.py @@ -0,0 +1,51 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from raillabel.format import OtherSensor +from raillabel.json_format import JSONCoordinateSystem, JSONStreamOther + +# == Fixtures ========================= + + +@pytest.fixture +def other_json(transform_json) -> tuple[JSONStreamOther, JSONCoordinateSystem]: + return ( + JSONStreamOther( + type="other", + uri="/path/to/sensor/data", + description="A very nice generic sensor", + ), + JSONCoordinateSystem( + parent="base", type="sensor", pose_wrt_parent=transform_json, children=None + ), + ) + + +@pytest.fixture +def other(transform) -> dict: + return OtherSensor( + extrinsics=transform, + uri="/path/to/sensor/data", + description="A very nice generic sensor", + ) + + +# == Tests ============================ + + +def test_from_json(other, other_json): + actual = OtherSensor.from_json(other_json[0], other_json[1]) + assert actual == other + + +def test_to_json(other, other_json): + actual = other.to_json() + assert actual == other_json + + +if __name__ == "__main__": + pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_point2d.py b/tests/test_raillabel/format/test_point2d.py index b72ee4c..54d3302 100644 --- a/tests/test_raillabel/format/test_point2d.py +++ b/tests/test_raillabel/format/test_point2d.py @@ -11,47 +11,46 @@ @pytest.fixture -def point2d_dict() -> dict: +def point2d_json() -> tuple[float, float]: return [1.5, 222] @pytest.fixture -def point2d() -> dict: +def point2d() -> Point2d: return Point2d(1.5, 222) @pytest.fixture -def point2d_another_dict() -> dict: - return [19, 84] +def another_point2d_json() -> tuple[float, float]: + return [1.7, 222.2] @pytest.fixture -def point2d_another() -> dict: - return Point2d(19, 84) +def another_point2d() -> Point2d: + return Point2d(1.7, 222.2) # == Tests ============================ -def test_from_json(point2d, point2d_dict): - actual = Point2d.from_json(point2d_dict) +def test_from_json(point2d, point2d_json): + actual = Point2d.from_json(point2d_json) assert actual == point2d -def test_fromdict(): - point2d = Point2d.fromdict([1.5, 222]) +def test_from_json__another(another_point2d, another_point2d_json): + actual = Point2d.from_json(another_point2d_json) + assert actual == another_point2d - assert point2d.x == 1.5 - assert point2d.y == 222 +def test_to_json(point2d, point2d_json): + actual = point2d.to_json() + assert actual == tuple(point2d_json) -def test_asdict(): - point2d = Point2d( - x=1.5, - y=222, - ) - assert point2d.asdict() == [1.5, 222] +def test_to_json__another(another_point2d, another_point2d_json): + actual = another_point2d.to_json() + assert actual == tuple(another_point2d_json) if __name__ == "__main__": diff --git a/tests/test_raillabel/format/test_point3d.py b/tests/test_raillabel/format/test_point3d.py index 238f226..1fef803 100644 --- a/tests/test_raillabel/format/test_point3d.py +++ b/tests/test_raillabel/format/test_point3d.py @@ -3,21 +3,15 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Point3d # == Fixtures ========================= @pytest.fixture -def point3d_dict() -> dict: +def point3d_json() -> dict: return [419, 3.14, 0] @@ -27,41 +21,37 @@ def point3d() -> dict: @pytest.fixture -def point3d_another_dict() -> dict: - return [9, 8, 7] +def another_point3d_json() -> dict: + return [419.2, 3.34, 0.2] @pytest.fixture -def point3d_another() -> dict: - return Point3d(9, 8, 7) +def another_point3d() -> dict: + return Point3d(419.2, 3.34, 0.2) # == Tests ============================ -def test_from_json(point3d, point3d_dict): - actual = Point3d.from_json(point3d_dict) +def test_from_json(point3d, point3d_json): + actual = Point3d.from_json(point3d_json) assert actual == point3d -def test_fromdict(): - point3d = Point3d.fromdict([419, 3.14, 0]) +def test_from_json__another(another_point3d, another_point3d_json): + actual = Point3d.from_json(another_point3d_json) + assert actual == another_point3d - assert point3d.x == 419 - assert point3d.y == 3.14 - assert point3d.z == 0 +def test_to_json(point3d, point3d_json): + actual = point3d.to_json() + assert actual == tuple(point3d_json) -def test_asdict(): - point3d = Point3d( - x=419, - y=3.14, - z=0, - ) - assert point3d.asdict() == [419, 3.14, 0] +def test_to_json__another(another_point3d, another_point3d_json): + actual = another_point3d.to_json() + assert actual == tuple(another_point3d_json) if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_poly2d.py b/tests/test_raillabel/format/test_poly2d.py index 24baa98..c193197 100644 --- a/tests/test_raillabel/format/test_poly2d.py +++ b/tests/test_raillabel/format/test_poly2d.py @@ -3,118 +3,71 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Poly2d +from raillabel.json_format import JSONPoly2d # == Fixtures ========================= @pytest.fixture -def poly2d_dict( - sensor_camera, attributes_multiple_types_dict, point2d_dict, point2d_another_dict -) -> dict: - return { - "uid": "d73b5988-767B-47ef-979c-022af60c6ab2", - "name": "rgb_middle__poly2d__person", - "val": point2d_dict + point2d_another_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - "mode": "MODE_POLY2D_ABSOLUTE", - } +def poly2d_json( + point2d_json, + another_point2d_json, + attributes_multiple_types_json, +) -> JSONPoly2d: + return JSONPoly2d( + uid="013e7b34-62E5-435c-9412-87318c50f6d8", + name="rgb_middle__poly2d__track", + closed=True, + mode="MODE_POLY2D_ABSOLUTE", + val=point2d_json + another_point2d_json, + coordinate_system="rgb_middle", + attributes=attributes_multiple_types_json, + ) + + +@pytest.fixture +def poly2d_id() -> UUID: + return UUID("013e7b34-62E5-435c-9412-87318c50f6d8") @pytest.fixture def poly2d( - point2d, point2d_another, sensor_camera, attributes_multiple_types, object_person -) -> dict: + point2d, + another_point2d, + attributes_multiple_types, + object_track_id, +) -> Poly2d: return Poly2d( - uid="d73b5988-767B-47ef-979c-022af60c6ab2", - points=[point2d, point2d_another], - object=object_person, - sensor=sensor_camera, - attributes=attributes_multiple_types, + points=[point2d, another_point2d], closed=True, - mode="MODE_POLY2D_ABSOLUTE", + sensor_id="rgb_middle", + attributes=attributes_multiple_types, + object_id=object_track_id, ) # == Tests ============================ -def test_fromdict( - point2d, - point2d_dict, - point2d_another, - point2d_another_dict, - sensor_camera, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - poly2d = Poly2d.fromdict( - { - "uid": "d73b5988-767B-47ef-979c-022af60c6ab2", - "name": "rgb_middle__poly2d__person", - "val": point2d_dict + point2d_another_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - "mode": "MODE_POLY2D_ABSOLUTE", - }, - sensors, - object_person, - ) +def test_from_json(poly2d, poly2d_json, object_track_id): + actual = Poly2d.from_json(poly2d_json, object_track_id) + assert actual == poly2d - assert poly2d.uid == "d73b5988-767B-47ef-979c-022af60c6ab2" - assert poly2d.name == "rgb_middle__poly2d__person" - assert poly2d.points == [point2d, point2d_another] - assert poly2d.object == object_person - assert poly2d.sensor == sensor_camera - assert poly2d.attributes == attributes_multiple_types - assert poly2d.closed == True - assert poly2d.mode == "MODE_POLY2D_ABSOLUTE" +def test_name(poly2d): + actual = poly2d.name("track") + assert actual == "rgb_middle__poly2d__track" -def test_asdict( - point2d, - point2d_dict, - point2d_another, - point2d_another_dict, - sensor_camera, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - poly2d = Poly2d( - uid="d73b5988-767B-47ef-979c-022af60c6ab2", - points=[point2d, point2d_another], - object=object_person, - sensor=sensor_camera, - attributes=attributes_multiple_types, - closed=True, - mode="MODE_POLY2D_ABSOLUTE", - ) - assert poly2d.asdict() == { - "uid": "d73b5988-767B-47ef-979c-022af60c6ab2", - "name": "rgb_middle__poly2d__person", - "val": point2d_dict + point2d_another_dict, - "coordinate_system": sensor_camera.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - "mode": "MODE_POLY2D_ABSOLUTE", - } +def test_to_json(poly2d, poly2d_json, poly2d_id): + actual = poly2d.to_json(poly2d_id, object_type="track") + assert actual == poly2d_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_poly3d.py b/tests/test_raillabel/format/test_poly3d.py index 87d8dec..e62d77e 100644 --- a/tests/test_raillabel/format/test_poly3d.py +++ b/tests/test_raillabel/format/test_poly3d.py @@ -3,110 +3,70 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Poly3d +from raillabel.json_format import JSONPoly3d # == Fixtures ========================= @pytest.fixture -def poly3d_dict( - sensor_lidar, attributes_multiple_types_dict, point3d_dict, point3d_another_dict -) -> dict: - return { - "uid": "9a9a30f5-D334-4f11-aa3f-c3c83f2935eb", - "name": "lidar__poly3d__person", - "val": point3d_dict + point3d_another_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - } +def poly3d_json( + point3d_json, + another_point3d_json, + attributes_multiple_types_json, +) -> JSONPoly3d: + return JSONPoly3d( + uid="0da87210-46F1-40e5-b661-20ea1c392f50", + name="lidar__poly3d__track", + closed=True, + val=point3d_json + another_point3d_json, + coordinate_system="lidar", + attributes=attributes_multiple_types_json, + ) @pytest.fixture -def poly3d(point3d, point3d_another, sensor_lidar, object_person, attributes_multiple_types) -> dict: +def poly3d_id() -> UUID: + return UUID("0da87210-46F1-40e5-b661-20ea1c392f50") + + +@pytest.fixture +def poly3d( + point3d, + another_point3d, + attributes_multiple_types, + object_track_id, +) -> Poly3d: return Poly3d( - uid="9a9a30f5-D334-4f11-aa3f-c3c83f2935eb", - points=[point3d, point3d_another], - object=object_person, - sensor=sensor_lidar, - attributes=attributes_multiple_types, + points=[point3d, another_point3d], closed=True, + sensor_id="lidar", + attributes=attributes_multiple_types, + object_id=object_track_id, ) # == Tests ============================ -def test_fromdict( - point3d, - point3d_dict, - point3d_another, - point3d_another_dict, - sensor_lidar, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - poly3d = Poly3d.fromdict( - { - "uid": "9a9a30f5-D334-4f11-aa3f-c3c83f2935eb", - "name": "lidar__poly3d__person", - "val": point3d_dict + point3d_another_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - }, - sensors, - object_person, - ) +def test_from_json(poly3d, poly3d_json, object_track_id): + actual = Poly3d.from_json(poly3d_json, object_track_id) + assert actual == poly3d - assert poly3d.uid == "9a9a30f5-D334-4f11-aa3f-c3c83f2935eb" - assert poly3d.name == "lidar__poly3d__person" - assert poly3d.points == [point3d, point3d_another] - assert poly3d.object == object_person - assert poly3d.sensor == sensor_lidar - assert poly3d.attributes == attributes_multiple_types - assert poly3d.closed == True +def test_name(poly3d): + actual = poly3d.name("track") + assert actual == "lidar__poly3d__track" -def test_asdict( - point3d, - point3d_dict, - point3d_another, - point3d_another_dict, - sensor_lidar, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - poly3d = Poly3d( - uid="9a9a30f5-D334-4f11-aa3f-c3c83f2935eb", - points=[point3d, point3d_another], - object=object_person, - sensor=sensor_lidar, - attributes=attributes_multiple_types, - closed=True, - ) - assert poly3d.asdict() == { - "uid": "9a9a30f5-D334-4f11-aa3f-c3c83f2935eb", - "name": "lidar__poly3d__person", - "val": point3d_dict + point3d_another_dict, - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - "closed": True, - } +def test_to_json(poly3d, poly3d_json, poly3d_id): + actual = poly3d.to_json(poly3d_id, object_type="track") + assert actual == poly3d_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_quaternion.py b/tests/test_raillabel/format/test_quaternion.py index d508e85..bff5656 100644 --- a/tests/test_raillabel/format/test_quaternion.py +++ b/tests/test_raillabel/format/test_quaternion.py @@ -3,21 +3,15 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Quaternion # == Fixtures ========================= @pytest.fixture -def quaternion_dict() -> dict: +def quaternion_json() -> dict: return [0.75318325, -0.10270147, 0.21430262, -0.61338551] @@ -29,26 +23,15 @@ def quaternion() -> dict: # == Tests ============================ -def test_from_json(quaternion, quaternion_dict): - actual = Quaternion.from_json(quaternion_dict) +def test_from_json(quaternion, quaternion_json): + actual = Quaternion.from_json(quaternion_json) assert actual == quaternion -def test_fromdict(): - quaternion = Quaternion.fromdict([0.75318325, -0.10270147, 0.21430262, -0.61338551]) - - assert quaternion.x == 0.75318325 - assert quaternion.y == -0.10270147 - assert quaternion.z == 0.21430262 - assert quaternion.w == -0.61338551 - - -def test_asdict(): - quaternion = Quaternion(x=0.75318325, y=-0.10270147, z=0.21430262, w=-0.61338551) - - assert quaternion.asdict() == [0.75318325, -0.10270147, 0.21430262, -0.61338551] +def test_to_json(quaternion, quaternion_json): + actual = quaternion.to_json() + assert actual == tuple(quaternion_json) if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_radar.py b/tests/test_raillabel/format/test_radar.py new file mode 100644 index 0000000..5bd8958 --- /dev/null +++ b/tests/test_raillabel/format/test_radar.py @@ -0,0 +1,60 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from raillabel.format import Radar +from raillabel.json_format import ( + JSONCoordinateSystem, + JSONStreamRadar, + JSONStreamRadarProperties, + JSONIntrinsicsRadar, +) + +# == Fixtures ========================= + + +@pytest.fixture +def radar_json( + intrinsics_radar_json, transform_json +) -> tuple[JSONStreamRadar, JSONCoordinateSystem]: + return ( + JSONStreamRadar( + type="radar", + stream_properties=JSONStreamRadarProperties(intrinsics_radar=intrinsics_radar_json), + uri="/path/to/sensor/data", + description="A very nice radar", + ), + JSONCoordinateSystem( + parent="base", type="sensor", pose_wrt_parent=transform_json, children=None + ), + ) + + +@pytest.fixture +def radar(intrinsics_radar, transform) -> dict: + return Radar( + intrinsics=intrinsics_radar, + extrinsics=transform, + uri="/path/to/sensor/data", + description="A very nice radar", + ) + + +# == Tests ============================ + + +def test_from_json(radar, radar_json): + actual = Radar.from_json(radar_json[0], radar_json[1]) + assert actual == radar + + +def test_to_json(radar, radar_json): + actual = radar.to_json() + assert actual == radar_json + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/test_raillabel/format/test_scene.py b/tests/test_raillabel/format/test_scene.py index 124b55d..b7dbe24 100644 --- a/tests/test_raillabel/format/test_scene.py +++ b/tests/test_raillabel/format/test_scene.py @@ -3,360 +3,99 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -from raillabel.format import Frame, FrameInterval, Scene -from raillabel.format.scene import _clean_dict +from raillabel.format import Scene +from raillabel.json_format import ( + JSONScene, + JSONSceneContent, + JSONCoordinateSystem, + JSONFrameInterval, +) # == Fixtures ========================= @pytest.fixture -def scene_dict( - metadata_full_dict, - sensor_camera_dict, - sensor_lidar_dict, - sensor_radar_dict, - object_person_dict, - object_train_dict, - frame, - frame_dict, -) -> dict: - return { - "openlabel": { - "metadata": metadata_full_dict, - "streams": { - sensor_camera_dict["uid"]: sensor_camera_dict["stream"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["stream"], - sensor_radar_dict["uid"]: sensor_radar_dict["stream"], - }, - "coordinate_systems": { - "base": { - "type": "local", - "parent": "", - "children": [ - sensor_lidar_dict["uid"], - sensor_camera_dict["uid"], - sensor_radar_dict["uid"], - ], - }, - sensor_camera_dict["uid"]: sensor_camera_dict["coordinate_system"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["coordinate_system"], - sensor_radar_dict["uid"]: sensor_radar_dict["coordinate_system"], +def scene_json( + metadata_json, + camera_json, + lidar_json, + radar_json, + object_person_id, + object_person_json, + object_track_id, + object_track_json, + frame_json, +) -> JSONScene: + return JSONScene( + openlabel=JSONSceneContent( + metadata=metadata_json, + coordinate_systems={ + "base": JSONCoordinateSystem( + parent="", + type="local", + pose_wrt_parent=None, + children=["rgb_middle", "lidar", "radar"], + ), + "rgb_middle": camera_json[1], + "lidar": lidar_json[1], + "radar": radar_json[1], }, - "objects": { - object_person_dict["object_uid"]: object_person_dict["data_dict"], - object_train_dict["object_uid"]: object_train_dict["data_dict"], + streams={ + "rgb_middle": camera_json[0], + "lidar": lidar_json[0], + "radar": radar_json[0], }, - "frames": { - frame.uid: frame_dict, + objects={ + object_person_id: object_person_json, + object_track_id: object_track_json, }, - "frame_intervals": [ - { - "frame_start": 0, - "frame_end": 0, - } - ], - } - } + frames={1: frame_json}, + frame_intervals=[JSONFrameInterval(frame_start=1, frame_end=1)], + ) + ) @pytest.fixture def scene( - metadata_full, sensor_camera, sensor_lidar, sensor_radar, object_person, object_train, frame + metadata, + camera, + lidar, + radar, + object_person_id, + object_person, + object_track_id, + object_track, + frame, ) -> Scene: return Scene( - metadata=metadata_full, + metadata=metadata, sensors={ - sensor_lidar.uid: sensor_lidar, - sensor_camera.uid: sensor_camera, - sensor_radar.uid: sensor_radar, + "rgb_middle": camera, + "lidar": lidar, + "radar": radar, }, objects={ - object_person.uid: object_person, - object_train.uid: object_train, + object_person_id: object_person, + object_track_id: object_track, }, - frames={frame.uid: frame}, + frames={1: frame}, ) # == Tests ============================ -def test_fromdict_metadata( - metadata_full, - metadata_full_dict, -): - scene = Scene.fromdict( - { - "openlabel": { - "metadata": metadata_full_dict, - } - }, - subschema_version=metadata_full.subschema_version, - ) - - scene.metadata.exporter_version = None # necessary for testing on remote - - assert scene.metadata == metadata_full - - -def test_fromdict_sensors( - metadata_full_dict, - sensor_camera, - sensor_lidar, - sensor_radar, - sensor_camera_dict, - sensor_lidar_dict, - sensor_radar_dict, -): - scene = Scene.fromdict( - { - "openlabel": { - "metadata": metadata_full_dict, - "streams": { - sensor_camera_dict["uid"]: sensor_camera_dict["stream"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["stream"], - sensor_radar_dict["uid"]: sensor_radar_dict["stream"], - }, - "coordinate_systems": { - "base": { - "type": "local", - "parent": "", - "children": [ - sensor_lidar_dict["uid"], - sensor_camera_dict["uid"], - sensor_radar_dict["uid"], - ], - }, - sensor_camera_dict["uid"]: sensor_camera_dict["coordinate_system"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["coordinate_system"], - sensor_radar_dict["uid"]: sensor_radar_dict["coordinate_system"], - }, - } - } - ) - - assert scene.sensors == { - sensor_lidar.uid: sensor_lidar, - sensor_camera.uid: sensor_camera, - sensor_radar.uid: sensor_radar, - } - - -def test_fromdict_objects( - metadata_full, - metadata_full_dict, - object_person, - object_train, - object_person_dict, - object_train_dict, -): - scene = Scene.fromdict( - { - "openlabel": { - "metadata": metadata_full_dict, - "objects": { - object_person_dict["object_uid"]: object_person_dict["data_dict"], - object_train_dict["object_uid"]: object_train_dict["data_dict"], - }, - } - }, - subschema_version=metadata_full.subschema_version, - ) - - assert scene.objects == { - object_person.uid: object_person, - object_train.uid: object_train, - } - - -def test_fromdict_frames( - metadata_full, - metadata_full_dict, - streams_dict, - coordinate_systems_dict, - objects_dict, - frame, - frame_dict, -): - scene = Scene.fromdict( - { - "openlabel": { - "metadata": metadata_full_dict, - "streams": streams_dict, - "coordinate_systems": coordinate_systems_dict, - "objects": objects_dict, - "frames": { - "0": frame_dict, - }, - "frame_intervals": [ - { - "frame_start": 0, - "frame_end": 0, - } - ], - } - }, - subschema_version=metadata_full.subschema_version, - ) - - assert scene.frames == { - 0: frame, - } - - -def test_asdict_sensors( - metadata_full, - metadata_full_dict, - sensor_camera, - sensor_lidar, - sensor_radar, - sensor_camera_dict, - sensor_lidar_dict, - sensor_radar_dict, -): - scene = Scene( - metadata=metadata_full, - sensors={ - sensor_lidar.uid: sensor_lidar, - sensor_camera.uid: sensor_camera, - sensor_radar.uid: sensor_radar, - }, - ) - - assert scene.asdict() == { - "openlabel": { - "metadata": metadata_full_dict, - "streams": { - sensor_camera_dict["uid"]: sensor_camera_dict["stream"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["stream"], - sensor_radar_dict["uid"]: sensor_radar_dict["stream"], - }, - "coordinate_systems": { - "base": { - "type": "local", - "parent": "", - "children": [ - sensor_lidar_dict["uid"], - sensor_camera_dict["uid"], - sensor_radar_dict["uid"], - ], - }, - sensor_camera_dict["uid"]: sensor_camera_dict["coordinate_system"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["coordinate_system"], - sensor_radar_dict["uid"]: sensor_radar_dict["coordinate_system"], - }, - } - } - - -def test_asdict_objects( - metadata_full, - metadata_full_dict, - object_person, - object_train, - object_person_dict, - object_train_dict, -): - scene = Scene( - metadata=metadata_full, - objects={ - object_person.uid: object_person, - object_train.uid: object_train, - }, - ) - - assert scene.asdict(calculate_pointers=False) == { - "openlabel": { - "metadata": metadata_full_dict, - "objects": { - object_person_dict["object_uid"]: object_person_dict["data_dict"], - object_train_dict["object_uid"]: object_train_dict["data_dict"], - }, - } - } - - -def test_asdict_frames( - metadata_full, - metadata_full_dict, - sensors, - streams_dict, - coordinate_systems_dict, - objects, - objects_dict, - frame, - frame_dict, -): - scene = Scene( - metadata=metadata_full, - sensors=sensors, - objects=objects, - frames={ - "0": frame, - }, - ) - - assert scene.asdict(calculate_pointers=False) == { - "openlabel": { - "metadata": metadata_full_dict, - "streams": streams_dict, - "coordinate_systems": coordinate_systems_dict, - "objects": objects_dict, - "frames": { - "0": frame_dict, - }, - "frame_intervals": [ - { - "frame_start": 0, - "frame_end": 0, - } - ], - } - } - - -def test_frame_intervals(metadata_minimal): - scene = Scene( - metadata=metadata_minimal, - frames={ - 1: Frame(1), - 2: Frame(2), - 3: Frame(3), - 8: Frame(8), - }, - ) - - assert scene.frame_intervals == [ - FrameInterval(1, 3), - FrameInterval(8, 8), - ] - - -def test_integration(json_data): - scene_dict = json_data["openlabel_v1_short"] - - actual = Scene.fromdict(scene_dict).asdict() - - del actual["openlabel"]["metadata"]["exporter_version"] - assert actual == scene_dict - +def test_from_json(scene, scene_json): + actual = Scene.from_json(scene_json) + assert actual == scene -def test_clean_dict(): - input_dict = {"non_empty_field": "non_empty_value", "none_field": None, "field_with_len_0": []} - assert _clean_dict(input_dict) == { - "non_empty_field": "non_empty_value", - } +def test_to_json(scene, scene_json): + actual = scene.to_json() + assert actual == scene_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-vv"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_seg3d.py b/tests/test_raillabel/format/test_seg3d.py index 1ff15e3..a290aa6 100644 --- a/tests/test_raillabel/format/test_seg3d.py +++ b/tests/test_raillabel/format/test_seg3d.py @@ -3,95 +3,64 @@ from __future__ import annotations -import os -import sys -from pathlib import Path +from uuid import UUID import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Seg3d +from raillabel.json_format import JSONVec # == Fixtures ========================= @pytest.fixture -def seg3d_dict(sensor_lidar, attributes_multiple_types_dict) -> dict: - return { - "uid": "db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a", - "name": "lidar__vec__person", - "val": [586, 789, 173], - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - } +def seg3d_json( + attributes_multiple_types_json, +) -> JSONVec: + return JSONVec( + uid="d52e2b25-0B48-4899-86d5-4bc41be6b7d3", + name="lidar__vec__person", + val=[1234, 5678], + coordinate_system="lidar", + attributes=attributes_multiple_types_json, + ) + + +@pytest.fixture +def seg3d_id() -> UUID: + return UUID("d52e2b25-0B48-4899-86d5-4bc41be6b7d3") @pytest.fixture -def seg3d(sensor_lidar, attributes_multiple_types, object_person) -> dict: +def seg3d( + attributes_multiple_types, + object_person_id, +) -> Seg3d: return Seg3d( - uid="db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a", - point_ids=[586, 789, 173], - object=object_person, - sensor=sensor_lidar, + point_ids=[1234, 5678], + sensor_id="lidar", attributes=attributes_multiple_types, + object_id=object_person_id, ) # == Tests ============================ -def test_fromdict( - sensor_lidar, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - seg3d = Seg3d.fromdict( - { - "uid": "db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a", - "name": "lidar__vec__person", - "val": [586, 789, 173], - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - }, - sensors, - object_person, - ) +def test_from_json(seg3d, seg3d_json, object_person_id): + actual = Seg3d.from_json(seg3d_json, object_person_id) + assert actual == seg3d - assert seg3d.uid == "db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a" - assert seg3d.name == "lidar__vec__person" - assert seg3d.point_ids == [586, 789, 173] - assert seg3d.object == object_person - assert seg3d.sensor == sensor_lidar - assert seg3d.attributes == attributes_multiple_types +def test_name(seg3d): + actual = seg3d.name("person") + assert actual == "lidar__vec__person" -def test_asdict( - sensor_lidar, - sensors, - object_person, - attributes_multiple_types, - attributes_multiple_types_dict, -): - seg3d = Seg3d( - uid="db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a", - point_ids=[586, 789, 173], - object=object_person, - sensor=sensor_lidar, - attributes=attributes_multiple_types, - ) - assert seg3d.asdict() == { - "uid": "db4e4a77-B926-4a6c-a2a6-e0ecf9d8734a", - "name": "lidar__vec__person", - "val": [586, 789, 173], - "coordinate_system": sensor_lidar.uid, - "attributes": attributes_multiple_types_dict, - } +def test_to_json(seg3d, seg3d_json, seg3d_id): + actual = seg3d.to_json(seg3d_id, object_type="person") + assert actual == seg3d_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_sensor.py b/tests/test_raillabel/format/test_sensor.py deleted file mode 100644 index 59b2259..0000000 --- a/tests/test_raillabel/format/test_sensor.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import os -import sys -import typing as t -from pathlib import Path - -import pytest - -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - -from raillabel.format import Sensor, SensorType - -# == Fixtures ========================= - - -@pytest.fixture -def sensors(sensor_lidar, sensor_camera, sensor_radar) -> dict[str, Sensor]: - return { - sensor_lidar.uid: sensor_lidar, - sensor_camera.uid: sensor_camera, - sensor_radar.uid: sensor_radar, - } - - -@pytest.fixture -def streams_dict(sensor_camera_dict, sensor_lidar_dict, sensor_radar_dict) -> dict: - return { - sensor_camera_dict["uid"]: sensor_camera_dict["stream"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["stream"], - sensor_radar_dict["uid"]: sensor_radar_dict["stream"], - } - - -@pytest.fixture -def coordinate_systems_dict(sensor_camera_dict, sensor_lidar_dict, sensor_radar_dict) -> dict: - return { - "base": { - "type": "local", - "parent": "", - "children": [ - sensor_lidar_dict["uid"], - sensor_camera_dict["uid"], - sensor_radar_dict["uid"], - ], - }, - sensor_camera_dict["uid"]: sensor_camera_dict["coordinate_system"], - sensor_lidar_dict["uid"]: sensor_lidar_dict["coordinate_system"], - sensor_radar_dict["uid"]: sensor_radar_dict["coordinate_system"], - } - - -@pytest.fixture -def sensor_lidar_dict(transform_dict) -> dict: - return { - "uid": "lidar", - "stream": { - "type": "lidar", - "uri": "/lidar_merged", - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -@pytest.fixture -def sensor_lidar(transform) -> Sensor: - return Sensor( - uid="lidar", - extrinsics=transform, - intrinsics=None, - type=SensorType.LIDAR, - uri="/lidar_merged", - ) - - -@pytest.fixture -def sensor_camera_dict(transform_dict, intrinsics_pinhole_dict) -> dict: - return { - "uid": "rgb_middle", - "stream": { - "type": "camera", - "uri": "/S1206063/image", - "stream_properties": {"intrinsics_pinhole": intrinsics_pinhole_dict}, - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -@pytest.fixture -def sensor_camera(transform, intrinsics_pinhole) -> Sensor: - return Sensor( - uid="rgb_middle", - extrinsics=transform, - intrinsics=intrinsics_pinhole, - type=SensorType.CAMERA, - uri="/S1206063/image", - ) - - -@pytest.fixture -def sensor_radar_dict(transform_dict, intrinsics_radar_dict) -> dict: - return { - "uid": "radar", - "stream": { - "type": "radar", - "uri": "/talker1/Nvt/Cartesian", - "stream_properties": {"intrinsics_radar": intrinsics_radar_dict}, - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -@pytest.fixture -def sensor_radar(transform, intrinsics_radar) -> Sensor: - return Sensor( - uid="radar", - extrinsics=transform, - intrinsics=intrinsics_radar, - type=SensorType.RADAR, - uri="/talker1/Nvt/Cartesian", - ) - - -# == Tests ============================ - - -def test_lidar_fromdict(transform, transform_dict): - sensor = Sensor.fromdict( - uid="lidar", - stream_data_dict={ - "type": "lidar", - "uri": "/lidar_merged", - }, - cs_data_dict={ - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - ) - - assert sensor.uid == "lidar" - assert sensor.extrinsics == transform - assert sensor.intrinsics == None - assert sensor.type == SensorType.LIDAR - assert sensor.uri == "/lidar_merged" - - -def test_lidar_asdict(transform, transform_dict): - sensor = Sensor( - uid="lidar", - extrinsics=transform, - intrinsics=None, - type=SensorType.LIDAR, - uri="/lidar_merged", - ) - - assert sensor.asdict() == { - "stream": { - "type": "lidar", - "uri": "/lidar_merged", - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -def test_camera_fromdict(transform, transform_dict, intrinsics_pinhole, intrinsics_pinhole_dict): - sensor = Sensor.fromdict( - uid="rgb_middle", - stream_data_dict={ - "type": "camera", - "uri": "/S1206063/image", - "stream_properties": {"intrinsics_pinhole": intrinsics_pinhole_dict}, - }, - cs_data_dict={ - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - ) - - assert sensor.uid == "rgb_middle" - assert sensor.extrinsics == transform - assert sensor.intrinsics == intrinsics_pinhole - assert sensor.type == SensorType.CAMERA - assert sensor.uri == "/S1206063/image" - - -def test_camera_asdict(transform, transform_dict, intrinsics_pinhole, intrinsics_pinhole_dict): - sensor = Sensor( - uid="rgb_middle", - extrinsics=transform, - intrinsics=intrinsics_pinhole, - type=SensorType.CAMERA, - uri="/S1206063/image", - ) - - assert sensor.asdict() == { - "stream": { - "type": "camera", - "uri": "/S1206063/image", - "stream_properties": {"intrinsics_pinhole": intrinsics_pinhole_dict}, - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -def test_radar_fromdict(transform, transform_dict, intrinsics_radar, intrinsics_radar_dict): - sensor = Sensor.fromdict( - uid="radar", - stream_data_dict={ - "type": "radar", - "uri": "/talker1/Nvt/Cartesian", - "stream_properties": {"intrinsics_radar": intrinsics_radar_dict}, - }, - cs_data_dict={ - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - ) - - assert sensor.uid == "radar" - assert sensor.extrinsics == transform - assert sensor.intrinsics == intrinsics_radar - assert sensor.type == SensorType.RADAR - assert sensor.uri == "/talker1/Nvt/Cartesian" - - -def test_radar_asdict(transform, transform_dict, intrinsics_radar, intrinsics_radar_dict): - sensor = Sensor( - uid="radar", - extrinsics=transform, - intrinsics=intrinsics_radar, - type=SensorType.RADAR, - uri="/talker1/Nvt/Cartesian", - ) - - assert sensor.asdict() == { - "stream": { - "type": "radar", - "uri": "/talker1/Nvt/Cartesian", - "stream_properties": {"intrinsics_radar": intrinsics_radar_dict}, - }, - "coordinate_system": { - "type": "sensor", - "parent": "base", - "pose_wrt_parent": transform_dict, - }, - } - - -if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) diff --git a/tests/test_raillabel/format/test_sensor_reference.py b/tests/test_raillabel/format/test_sensor_reference.py index 886b41c..95ed9d3 100644 --- a/tests/test_raillabel/format/test_sensor_reference.py +++ b/tests/test_raillabel/format/test_sensor_reference.py @@ -3,63 +3,67 @@ from __future__ import annotations -import os -import sys -from decimal import Decimal -from pathlib import Path - import pytest - -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) +from decimal import Decimal from raillabel.format import SensorReference +from raillabel.json_format import JSONStreamSync, JSONStreamSyncProperties, JSONStreamSyncTimestamp # == Fixtures ========================= @pytest.fixture -def sensor_reference_camera_dict() -> dict: - return { - "stream_properties": {"sync": {"timestamp": "1632321743.100000072"}}, - "uri": "rgb_test0.png", - } +def sensor_reference_json() -> JSONStreamSync: + return JSONStreamSync( + stream_properties=JSONStreamSyncProperties( + sync=JSONStreamSyncTimestamp(timestamp="1631337747.123123123") + ), + uri="/uri/to/file.png", + ) + + +@pytest.fixture +def sensor_reference() -> SensorReference: + return SensorReference(timestamp=Decimal("1631337747.123123123"), uri="/uri/to/file.png") @pytest.fixture -def sensor_reference_camera(sensor_camera) -> dict: - return SensorReference( - sensor=sensor_camera, timestamp=Decimal("1632321743.100000072"), uri="rgb_test0.png" +def another_sensor_reference_json() -> JSONStreamSync: + return JSONStreamSync( + stream_properties=JSONStreamSyncProperties( + sync=JSONStreamSyncTimestamp(timestamp="1631337747.103123123") + ), + uri="/uri/to/file.pcd", ) +@pytest.fixture +def another_sensor_reference() -> SensorReference: + return SensorReference(timestamp=Decimal("1631337747.103123123"), uri="/uri/to/file.pcd") + + # == Tests ============================ -def test_fromdict(sensor_camera): - sensor_reference = SensorReference.fromdict( - { - "stream_properties": {"sync": {"timestamp": "1632321743.100000072"}}, - "uri": "rgb_test0.png", - }, - sensor_camera, - ) +def test_from_json(sensor_reference, sensor_reference_json): + actual = SensorReference.from_json(sensor_reference_json) + assert actual == sensor_reference - assert sensor_reference.sensor == sensor_camera - assert sensor_reference.timestamp == Decimal("1632321743.100000072") - assert sensor_reference.uri == "rgb_test0.png" +def test_from_json__another(another_sensor_reference, another_sensor_reference_json): + actual = SensorReference.from_json(another_sensor_reference_json) + assert actual == another_sensor_reference + + +def test_to_json(sensor_reference, sensor_reference_json): + actual = sensor_reference.to_json() + assert actual == sensor_reference_json -def test_asdict(sensor_camera): - sensor_reference = SensorReference( - sensor=sensor_camera, timestamp=Decimal("1632321743.100000072"), uri="rgb_test0.png" - ) - assert sensor_reference.asdict() == { - "stream_properties": {"sync": {"timestamp": "1632321743.100000072"}}, - "uri": "rgb_test0.png", - } +def test_to_json__another(another_sensor_reference, another_sensor_reference_json): + actual = another_sensor_reference.to_json() + assert actual == another_sensor_reference_json if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_size2d.py b/tests/test_raillabel/format/test_size2d.py index 65ffd4f..3df8424 100644 --- a/tests/test_raillabel/format/test_size2d.py +++ b/tests/test_raillabel/format/test_size2d.py @@ -11,7 +11,7 @@ @pytest.fixture -def size2d_dict() -> dict: +def size2d_json() -> dict: return [25, 1.344] @@ -23,25 +23,14 @@ def size2d() -> dict: # == Tests ============================ -def test_from_json(size2d, size2d_dict): - actual = Size2d.from_json(size2d_dict) +def test_from_json(size2d, size2d_json): + actual = Size2d.from_json(size2d_json) assert actual == size2d -def test_fromdict(): - size2d = Size2d.fromdict([25, 1.344]) - - assert size2d.x == 25 - assert size2d.y == 1.344 - - -def test_asdict(): - size2d = Size2d( - x=25, - y=1.344, - ) - - assert size2d.asdict() == [25, 1.344] +def test_to_json(size2d, size2d_json): + actual = size2d.to_json() + assert actual == tuple(size2d_json) if __name__ == "__main__": diff --git a/tests/test_raillabel/format/test_size3d.py b/tests/test_raillabel/format/test_size3d.py index 8626da3..801c4fa 100644 --- a/tests/test_raillabel/format/test_size3d.py +++ b/tests/test_raillabel/format/test_size3d.py @@ -3,46 +3,35 @@ from __future__ import annotations -import os -import sys -from pathlib import Path - import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent.parent.parent)) - from raillabel.format import Size3d # == Fixtures ========================= @pytest.fixture -def size3d_dict() -> dict: - return [0.35, 0.7, 1.92] +def size3d_json() -> dict: + return [25, 1.344, 12.3] @pytest.fixture def size3d() -> dict: - return Size3d(0.35, 0.7, 1.92) + return Size3d(25, 1.344, 12.3) # == Tests ============================ -def test_fromdict(): - size3d = Size3d.fromdict([0.35, 0.7, 1.92]) - - assert size3d.x == 0.35 - assert size3d.y == 0.7 - assert size3d.z == 1.92 - +def test_from_json(size3d, size3d_json): + actual = Size3d.from_json(size3d_json) + assert actual == size3d -def test_asdict(): - size3d = Size3d(x=0.35, y=0.7, z=1.92) - assert size3d.asdict() == [0.35, 0.7, 1.92] +def test_to_json(size3d, size3d_json): + actual = size3d.to_json() + assert actual == tuple(size3d_json) if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/format/test_transform.py b/tests/test_raillabel/format/test_transform.py index f6bcd78..9f816dc 100644 --- a/tests/test_raillabel/format/test_transform.py +++ b/tests/test_raillabel/format/test_transform.py @@ -13,18 +13,13 @@ @pytest.fixture -def transform_dict(point3d_dict, quaternion_dict) -> dict: - return {"translation": point3d_dict, "quaternion": quaternion_dict} - - -@pytest.fixture -def transform_json(point3d_dict, quaternion_dict) -> JSONTransformData: - return JSONTransformData(translation=point3d_dict, quaternion=quaternion_dict) +def transform_json(point3d_json, quaternion_json) -> JSONTransformData: + return JSONTransformData(translation=point3d_json, quaternion=quaternion_json) @pytest.fixture def transform(point3d, quaternion) -> Transform: - return Transform(position=point3d, quaternion=quaternion) + return Transform(pos=point3d, quat=quaternion) # == Tests ============================ @@ -35,17 +30,9 @@ def test_from_json(transform_json, transform): assert actual == transform -def test_fromdict(point3d, point3d_dict, quaternion, quaternion_dict): - transform = Transform.fromdict({"translation": point3d_dict, "quaternion": quaternion_dict}) - - assert transform.position == point3d - assert transform.quaternion == quaternion - - -def test_asdict(point3d, point3d_dict, quaternion, quaternion_dict): - transform = Transform(position=point3d, quaternion=quaternion) - - assert transform.asdict() == {"translation": point3d_dict, "quaternion": quaternion_dict} +def test_to_json(transform, transform_json): + actual = transform.to_json() + assert actual == transform_json if __name__ == "__main__": diff --git a/tests/test_raillabel/load/test_load.py b/tests/test_raillabel/load/test_load.py index 7c95c19..a80670a 100644 --- a/tests/test_raillabel/load/test_load.py +++ b/tests/test_raillabel/load/test_load.py @@ -1,21 +1,19 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import os +from __future__ import annotations import pytest import raillabel -def test_load__contains_all_elements(json_paths): +def test_load(json_paths): actual = raillabel.load(json_paths["openlabel_v1_short"]) assert len(actual.sensors) == 4 assert len(actual.objects) == 3 assert len(actual.frames) == 2 -# Executes the test if the file is called if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear"]) + pytest.main([__file__, "-v"]) diff --git a/tests/test_raillabel/save/test_save.py b/tests/test_raillabel/save/test_save.py index fae68b1..4009a88 100644 --- a/tests/test_raillabel/save/test_save.py +++ b/tests/test_raillabel/save/test_save.py @@ -1,85 +1,23 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import json -import os -import sys -import tempfile -from copy import deepcopy -from pathlib import Path +from __future__ import annotations import pytest -sys.path.insert(1, str(Path(__file__).parent.parent.parent)) - import raillabel +from raillabel import Scene +from raillabel.json_format import JSONScene -def test_save_scene(json_paths): - with tempfile.TemporaryDirectory("w") as temp_dir: - scene_orig = raillabel.load(json_paths["openlabel_v1_short"]) - - raillabel.save(scene_orig, Path(temp_dir) / "test_save_file.json") - scene_saved = raillabel.load(Path(temp_dir) / "test_save_file.json") - - assert scene_orig == scene_saved - - -def test_save_json(json_data): - with tempfile.TemporaryDirectory("w") as temp_dir: - stripped_input_data = deepcopy(json_data["openlabel_v1_short"]) - - # Removes the object data pointers from the example file so that it needs to be generated from the data - for object in stripped_input_data["openlabel"]["objects"].values(): - del object["frame_intervals"] - del object["object_data_pointers"] - - with (Path(temp_dir) / "stripped_input_data.json").open("w") as f: - json.dump(stripped_input_data, f) - - scene = raillabel.load(Path(temp_dir) / "stripped_input_data.json") - raillabel.save(scene, Path(temp_dir) / "test_save_file.json") - - with (Path(temp_dir) / "test_save_file.json").open() as f: - saved_and_loaded_data = json.load(f) - - # Removes the exporter version and subschema version from the generated file as these are hard to test for - if "exporter_version" in saved_and_loaded_data["openlabel"]["metadata"]: - del saved_and_loaded_data["openlabel"]["metadata"]["exporter_version"] - - if "subschema_version" in saved_and_loaded_data["openlabel"]["metadata"]: - del saved_and_loaded_data["openlabel"]["metadata"]["subschema_version"] - - assert saved_and_loaded_data == json_data["openlabel_v1_short"] - - -def test_frame_intervals(): - data = { - "openlabel": { - "metadata": {"schema_version": "1.0.0"}, - "frames": { - "0": {}, - "1": {}, - "2": {}, - "5": {}, - "7": {}, - "8": {}, - }, - } - } - - scene = raillabel.Scene.fromdict(data) - dict_repr = scene.asdict()["openlabel"] +def test_save(json_data, tmp_path): + scene_path = tmp_path / "scene.json" + ground_truth_scene = Scene.from_json(JSONScene(**json_data["openlabel_v1_short"])) + raillabel.save(ground_truth_scene, scene_path) - assert "frame_intervals" in dict_repr - assert dict_repr["frame_intervals"] == [ - {"frame_start": 0, "frame_end": 2}, - {"frame_start": 5, "frame_end": 5}, - {"frame_start": 7, "frame_end": 8}, - ] + actual = raillabel.load(scene_path) + assert actual == ground_truth_scene -# Executes the test if the file is called if __name__ == "__main__": - os.system("clear") - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-vv"]) + pytest.main([__file__, "-v"])