Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deepen): add support of converting deepen 2D segmentation annotation #164

Merged
merged 24 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6c6fdf5
TODO: add deepen segmentation conversion
ktro2828 Oct 15, 2024
b166021
feat: add support of converting deepen 2D segmentation annotation
ktro2828 Oct 15, 2024
aa058a4
chore: update surface labels
ktro2828 Oct 30, 2024
2a273bc
fix avoid to use `if cam_idx < num_cameras`
ktro2828 Oct 30, 2024
ff19888
fix: strip `sensor` instead of `sensor_`
ktro2828 Oct 30, 2024
23aa304
chore: update surface.yaml
ktro2828 Oct 30, 2024
6076da6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2024
9936966
fix: use field if it has
ktro2828 Nov 7, 2024
4fb939e
Merge remote-tracking branch 'origin' into feat/deepen/segmentation2d
ktro2828 Nov 7, 2024
b6a7024
fix: resolve circular import
ktro2828 Nov 11, 2024
b9851dc
fix: set default of to
ktro2828 Nov 11, 2024
244a8dd
fix: update the condition of annotation types
ktro2828 Nov 12, 2024
b383354
Update perception_dataset/deepen/segmentation/painting2d.py
Shin-kyoto Nov 13, 2024
67f2a03
Update config/label/surface.yaml
Shin-kyoto Nov 13, 2024
4808357
Update config/convert_deepen_to_t4_segmetation_painting_sample.yaml
ktro2828 Nov 14, 2024
c27e527
Update config/convert_deepen_to_t4_segmetation_polygon_sample.yaml
ktro2828 Nov 14, 2024
2d1a2c8
chore: rename config files
ktro2828 Nov 14, 2024
7d5dcf4
feat: update comparing sensor id and camera index
ktro2828 Nov 14, 2024
10d4455
feat: update surface labels
ktro2828 Nov 14, 2024
181947d
feat: update comparing sensor index and camera index in painting
ktro2828 Nov 15, 2024
2e2aeeb
fix: remove invalid operation
ktro2828 Nov 19, 2024
fd9afab
Update perception_dataset/deepen/segmentation/polygon2d.py
ktro2828 Nov 21, 2024
76aaa19
Update perception_dataset/deepen/segmentation/polygon2d.py
ktro2828 Nov 21, 2024
8abb8ac
Merge branch 'main' into feat/deepen/segmentation2d
ktro2828 Nov 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions config/convert_deepen_to_t4_segmentation_painting_sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
task: convert_deepen_to_t4
description:
visibility:
full: "No occlusion of the object."
most: "Object is occluded, but by less than 50%."
partial: "The object is occluded by more than 50% (but not completely)."
none: "The object is 90-100% occluded and no points/pixels are visible in the label."
camera_index:
CAM_BACK_RIGHT: 0
CAM_BACK: 1
CAM_BACK_LEFT: 2
CAM_FRONT: 3
CAM_FRONT_LEFT: 4
CAM_FRONT_NARROW: 5
CAM_FRONT_RIGHT: 6
CAM_FRONT_WIDE: 7
with_lidar: false
surface_categories: ./config/label/surface.yaml

conversion:
label_info: # for 3D/2D box only annotations, it is OK to skip specifying this field
label_type: 2d_segmentation
label_format: painting
input_base: ./data/non_annotated_dataset
input_anno_file: ./data/deepen_format.zip
input_bag_base: ./data/rosbag2
output_base: ./data/t4_format
topic_list: ./config/topic_list_tlr.yaml
ignore_interpolate_label: True
dataset_corresponding:
Dataset_name: dataset_id_in_Deepen_AI
31 changes: 31 additions & 0 deletions config/convert_deepen_to_t4_segmentation_polygon_sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
task: convert_deepen_to_t4
description:
visibility:
full: "No occlusion of the object."
most: "Object is occluded, but by less than 50%."
partial: "The object is occluded by more than 50% (but not completely)."
none: "The object is 90-100% occluded and no points/pixels are visible in the label."
camera_index:
CAM_BACK_RIGHT: 0
CAM_BACK: 1
CAM_BACK_LEFT: 2
CAM_FRONT: 3
CAM_FRONT_LEFT: 4
CAM_FRONT_NARROW: 5
CAM_FRONT_RIGHT: 6
CAM_FRONT_WIDE: 7
with_lidar: false
surface_categories: ./config/label/surface.yaml

conversion:
label_info: # for 3D/2D box only annotations, it is OK to skip specifying this field
label_type: 2d_segmentation
label_format: polygon
input_base: ./data/non_annotated_t4_format
input_anno_file: ./data/deepen_format/lidar_annotations_accepted_deepen.json
input_bag_base: ./data/rosbag2
output_base: ./data/t4_format
topic_list: ./config/topic_list_sample.yaml
ignore_interpolate_label: True
dataset_corresponding:
Dataset_name: dataset_id_in_Deepen_AI
11 changes: 11 additions & 0 deletions config/label/surface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Category list of surfaces. Each category does not have "instance ID".
# Reference(TIER IV INTERNAL LINK): https://drive.google.com/file/d/132nKnXc7vq9Bjj0quoXDFGi9q5jtrXuY/view?usp=drive_link
- road
- sidewalk
- building
- wall_fence
- pole
- vegetation_terrain
- sky
- road_paint_lane_solid_white
- road_paint_lane_dash_white
7 changes: 7 additions & 0 deletions perception_dataset/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def main():
)

elif task == "convert_deepen_to_t4":
from perception_dataset.deepen.deepen_annotation import LabelInfo
from perception_dataset.deepen.deepen_to_t4_converter import DeepenToT4Converter

input_base = config_dict["conversion"]["input_base"]
Expand All @@ -164,6 +165,11 @@ def main():
ignore_interpolate_label = config_dict["conversion"]["ignore_interpolate_label"]
with open(topic_list_yaml_path) as f:
topic_list_yaml = yaml.safe_load(f)
label_info = (
LabelInfo(**config_dict["conversion"]["label_info"])
if config_dict["conversion"].get("label_info")
else None
)

converter = DeepenToT4Converter(
input_base=input_base,
Expand All @@ -175,6 +181,7 @@ def main():
input_bag_base=input_bag_base,
topic_list=topic_list_yaml,
ignore_interpolate_label=ignore_interpolate_label,
label_info=label_info,
)

logger.info(f"[BEGIN] Converting Deepen data ({input_base}) to T4 data ({output_base})")
Expand Down
222 changes: 222 additions & 0 deletions perception_dataset/deepen/deepen_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
from __future__ import annotations

from abc import ABC
from dataclasses import asdict, dataclass, field
from enum import Enum
import json
from numbers import Number
import re
from typing import Any, Dict, List, Optional, TypeVar

from typing_extensions import Self

__all__ = ["DeepenAnnotation", "DeepenAnnotationLike"]


class LabelType(str, Enum):
BBOX_3D = "3d_bbox"
BBOX_2D = "box"
SEGMENTATION_2D = "2d_segmentation"


class LabelFormat(str, Enum):
POLYGON = "polygon"
PAINTING = "painting"


@dataclass
class LabelInfo:
label_type: LabelType
label_format: LabelFormat

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Self:
return cls(**data)


@dataclass
class DeepenAnnotation(ABC):
"""
Represents a single segmentation annotation in Deepen format.

Attributes:
dataset_id (str): The ID of the dataset.
file_id (str): The file identifier, e.g., "0.pcd".
label_category_id (str): The category of the label, e.g., "car".
label_id (str): The unique identifier of the label, e.g., "car:1".
label_type (str): The type of the label, e.g., "3d_bbox", "box", "2d_segmentation".
sensor_id (str): The identifier of the sensor, e.g., "lidar", "camera1".
labeller_email (str): The email of the labeller. Defaults to "default@tier4.jp".
attributes (Optional[Dict[str, Any]]): Additional attributes of the annotation, e.g.,
{
"state": "moving",
"occlusion": "none",
"cycle_state": "with_rider"
}.
three_d_bbox (Optional[Dict[str, Any]]): The 3D bounding box data. e.g.,
{
"cx": ...,
"cy": ...,
"cz": ...,
"h": ...,
"l": ...,
"w": ...,
"quaternion": {"x": ..., "y": ..., "z": ..., "w": 0}
}.
box (Optional[List[float]]): The 2D bounding box data, e.g., [corner_x, corner_y, width, height].
two_d_mask (Optional[str]): Run-length encoding (RLE) of the 2D mask.

Note:
Exactly one of `three_d_bbox`, `two_d_box`, or `two_d_mask` must be provided.
"""

dataset_id: str
file_id: str
label_category_id: str
label_id: str
label_type: str
sensor_id: str
labeller_email: str = "default@tier4.jp"
attributes: Optional[Dict[str, Any]] = field(default_factory=dict)
three_d_bbox: Optional[Dict[str, Any]] = None
box: Optional[List[float]] = None
two_d_mask: Optional[str] = None

def __post_init__(self) -> None:
"""
Validates the annotation data after initialization.
Ensures that exactly one of three_d_bbox, two_d_box, or two_d_mask is provided
and that the provided annotation contains the required data.
"""

def _check_provided_annotations_exists() -> None:
# Ensures that exactly one of three_d_bbox, two_d_box, or two_d_mask is provided.
provided_annotations = (
self.three_d_bbox is not None,
self.box is not None,
self.two_d_mask is not None,
)
assert any(
provided_annotations
), "At least, one of three_d_bbox, two_d_box, or two_d_mask must be provided."

def _check_label_id() -> None:
"""
Checks if label_id follows the format '{label_category_id}:{int}'.
Raises:
ValueError: If label_id does not match the required format.
"""
pattern = rf"^{re.escape(self.label_category_id)}:\d+$"
if not re.match(pattern, self.label_id):
raise ValueError(
f"label_id '{self.label_id}' must follow the format '{{label_category_id}}:{{int}}'"
)

def _check_three_d_bbox() -> None:
"""
Checks if three_d_bbox contains the required keys:
'cx', 'cy', 'cz', 'h', 'l', 'w', and 'quaternion' with keys 'x', 'y', 'z', 'w'.

Raises:
AssertionError: If any required keys are missing.
"""
if self.three_d_bbox is None:
return

required_keys = ("cx", "cy", "cz", "h", "l", "w", "quaternion")
missing_keys = [key for key in required_keys if key not in self.three_d_bbox]
assert not missing_keys, f"three_d_bbox is missing keys: {missing_keys}"

# Check quaternion
quaternion = self.three_d_bbox["quaternion"]
quaternion_keys = ("x", "y", "z", "w")
missing_quaternion_keys = [key for key in quaternion_keys if key not in quaternion]
assert (
not missing_quaternion_keys
), f"quaternion is missing keys: {missing_quaternion_keys}"

def _check_two_d_box() -> None:
"""
Checks if two_d_box has four elements: [corner_x, corner_y, width, height].

Raises:
AssertionError: If the list does not have exactly four numerical elements.
"""
if self.box is None:
return

assert isinstance(self.box, list), "two_d_box must be a list."
assert len(self.box) == 4, "two_d_box must be a list of four elements."
assert all(
isinstance(value, Number) and value >= 0 for value in self.box
), "two_d_box elements must be numbers and greater than 0."

def _check_two_d_mask() -> None:
"""
Checks if two_d_mask is a non-empty string representing an RLE (Run-Length Encoding).
Args:
two_d_mask (str): The RLE string to validate.
Raises:
AssertionError: If two_d_mask is not a valid non-empty string.
"""
if self.two_d_mask is None:
return

assert isinstance(
self.two_d_mask["counts"], str
), "two_d_mask['counts'] must be a string."
assert self.two_d_mask[
"counts"
].strip(), "two_d_mask['counts'] must not be an empty string."

# Ensures that exactly one of three_d_bbox, two_d_box, or two_d_mask is provided.
_check_provided_annotations_exists()

# Checks if label_id follows the format '{label_category_id}:{int}'
_check_label_id()

# Checks three_d_box keys
_check_three_d_bbox()

# Checks two_d_box keys
_check_two_d_box()

# Checks two_d_mask keys
_check_two_d_mask()

def to_dict(self) -> Dict[str, Any]:
"""Converts the dataclass instance to a dictionary."""
return asdict(self)

@classmethod
def from_file(
cls,
ann_file: str,
*,
as_dict: bool = True,
) -> List[DeepenAnnotationLike | Dict[str, Any]]:
"""Load annotations from file(s).

Args:
ann_file (str): Annotation file (.json).
as_dict (bool, optional): Whether to output objects as dict or its instance.
Defaults to True.

Returns:
List[DeepenAnnotationLike | Dict[str, Any]]: List of annotations or dicts.
"""
with open(ann_file, "r") as f:
data = json.load(f)

labels: List[Dict[str, Any]] = data["labels"]

output = []
for label in labels:
if as_dict:
output.append(label)
else:
output.append(DeepenAnnotation(**label))
return output


DeepenAnnotationLike = TypeVar("DeepenAnnotationLike", bound=DeepenAnnotation)
Loading
Loading