Skip to content

Commit 28f72a2

Browse files
committed
feat: add evaluator
Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent dfa7efe commit 28f72a2

8 files changed

Lines changed: 184 additions & 14 deletions

File tree

docs/tutorials/evaluation.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Perception Evaluation
2+
3+
```python
4+
from t4_devkit.evaluation import PerceptionEvaluator, PerceptionEvaluationConfig, EvaluationTask
5+
6+
config = PerceptionEvaluationConfig(
7+
dataset="<PATH_TO_DATASET>",
8+
task=EvaluationTask.<TASK_ENUM>,
9+
)
10+
11+
evaluator = PerceptionEvaluator(config)
12+
```
13+
14+
## Sensing Evaluation
15+
16+
TBD

mkdocs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nav:
2020
- Home: cli/index.md
2121
- t4viz: cli/t4viz.md
2222
- t4sanity: cli/t4sanity.md
23+
- Evaluation: tutorials/evaluation.md
2324
- API References:
2425
- t4_devkit.tier4: apis/tier4.md
2526
- t4_devkit.helper: apis/helper.md

t4_devkit/evaluation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
from .result import * # noqa
66
from .metric import * # noqa
77
from .task import * # noqa
8+
from .config import * # noqa
9+
from .evaluator import * # noqa

t4_devkit/evaluation/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from attrs import define, field
4+
5+
from t4_devkit.filtering import FilterParams
6+
7+
from .matching import MatchingParams
8+
from .task import EvaluationTask
9+
10+
__all__ = ["PerceptionEvaluationConfig"]
11+
12+
13+
@define
14+
class PerceptionEvaluationConfig:
15+
"""Evaluation configuration for perception tasks."""
16+
17+
dataset: str
18+
task: EvaluationTask = field(converter=EvaluationTask)
19+
filtering: FilterParams = field(default=FilterParams())
20+
matching: MatchingParams = field(default=MatchingParams())

t4_devkit/evaluation/dataset.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, TypeVar
44

55
from attrs import define
66

77
from t4_devkit import Tier4
8-
from t4_devkit.dataclass import HomogeneousMatrix, TransformBuffer
8+
from t4_devkit.dataclass import BoxLike, HomogeneousMatrix, SegmentationPointCloud, TransformBuffer
9+
from t4_devkit.typing import NDArrayU8
910

1011
from .task import EvaluationTask
1112

1213
if TYPE_CHECKING:
13-
from t4_devkit.dataclass import BoxLike
1414
from t4_devkit.schema import EgoPose, Sensor
1515

1616

17+
EvaluationObjectLike = TypeVar(
18+
"EvaluationObjectLike",
19+
list[BoxLike], # boxes
20+
SegmentationPointCloud, # pointcloud
21+
NDArrayU8, # mask
22+
)
23+
24+
1725
__all__ = ["load_dataset", "FrameGroundTruth", "SceneGroundTruth"]
1826

1927

@@ -31,12 +39,16 @@ def load_dataset(data_root: str, task: EvaluationTask) -> SceneGroundTruth:
3139

3240
frames: list[FrameGroundTruth] = []
3341
for i, sample in enumerate(t4.sample):
34-
# annotation boxes
35-
boxes = (
36-
list(map(t4.get_box3d, sample.ann_3ds))
37-
if task.is_3d()
38-
else list(map(t4.get_box2d, sample.ann_2ds))
39-
)
42+
# annotations
43+
if task.is_segmentation():
44+
# TODO(ktro2828): add support of segmentation object
45+
raise NotImplementedError("Segmentation task is under construction.")
46+
else:
47+
annotations = (
48+
list(map(t4.get_box3d, sample.ann_3ds))
49+
if task.is_3d()
50+
else list(map(t4.get_box2d, sample.ann_2ds))
51+
)
4052

4153
# transformation matrix from ego to map
4254
ego_pose = _closest_ego_pose(t4, sample.timestamp)
@@ -51,7 +63,7 @@ def load_dataset(data_root: str, task: EvaluationTask) -> SceneGroundTruth:
5163
FrameGroundTruth(
5264
unix_time=sample.timestamp,
5365
frame_index=i,
54-
boxes=boxes,
66+
annotations=annotations,
5567
ego2map=ego2map,
5668
)
5769
)
@@ -84,13 +96,13 @@ class FrameGroundTruth:
8496
Attributes:
8597
unix_time (int): Unix timestamp.
8698
frame_index (int): Index number of the frame.
87-
boxes (list[BoxLike]): List of ground truth instances.
99+
annotations (EvaluationObjectLike): Set of ground truth instances.
88100
ego2map (HomogeneousMatrix): Transformation matrix from ego to map coordinate.
89101
"""
90102

91103
unix_time: int
92104
frame_index: int
93-
boxes: list[BoxLike]
105+
annotations: EvaluationObjectLike
94106
ego2map: HomogeneousMatrix
95107

96108

t4_devkit/evaluation/evaluator.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from .dataset import load_dataset
6+
from .matching import build_matcher
7+
from .result import FrameBoxMatch
8+
9+
if TYPE_CHECKING:
10+
from t4_devkit.dataclass import BoxLike
11+
12+
from .config import PerceptionEvaluationConfig
13+
from .dataset import EvaluationObjectLike, FrameGroundTruth
14+
15+
16+
class PerceptionEvaluator:
17+
"""Evaluation manager for perception tasks."""
18+
19+
def __init__(self, config: PerceptionEvaluationConfig) -> None:
20+
self.config = config
21+
22+
self.scene_ground_truth = load_dataset(data_root=self.config.dataset, task=self.config.task)
23+
self.matcher = build_matcher(params=self.config.matching)
24+
25+
self.frames: list[FrameBoxMatch] = []
26+
27+
def add_frame(
28+
self,
29+
unix_time: int,
30+
estimations: EvaluationObjectLike,
31+
) -> FrameBoxMatch | None:
32+
"""Add frame result.
33+
34+
Returns None if it failed to find the closest timestamp ground truth.
35+
36+
Args:
37+
unix_time (int): Current unix time.
38+
estimations (EvaluationObjectLike): Set of estimations at the current frame.
39+
40+
Returns:
41+
Return frame result only if it succeeded to find the closest timestamp ground truth,
42+
otherwise None.
43+
"""
44+
frame_ground_truth = self.scene_ground_truth.lookup_frame(
45+
unix_time=unix_time,
46+
tolerance=7500,
47+
)
48+
49+
if frame_ground_truth is None:
50+
return None
51+
52+
# TODO(ktro2828): add support of segmentation object
53+
if self.config.task.is_segmentation():
54+
raise NotImplementedError("Segmentation task is under construction.")
55+
else:
56+
frame = self._to_frame_box(
57+
unix_time=unix_time,
58+
estimations=estimations,
59+
frame_ground_truth=frame_ground_truth,
60+
)
61+
62+
self.frames.append(frame)
63+
64+
return frame
65+
66+
def _to_frame_box(
67+
self,
68+
unix_time: int,
69+
estimations: list[BoxLike],
70+
frame_ground_truth: FrameGroundTruth,
71+
) -> FrameBoxMatch:
72+
"""Match estimations to ground truths and convert to frame result.
73+
74+
Args:
75+
unix_time (int): Current unix time associated with estimations.
76+
estimations (list[BoxLike]): List of estimations.
77+
frame_ground_truth (FrameGroundTruth): Frame ground truth.
78+
79+
Returns:
80+
Frame result.
81+
"""
82+
matches = self.matcher(estimations, frame_ground_truth.annotations)
83+
84+
return FrameBoxMatch(
85+
unix_time=unix_time,
86+
frame_index=frame_ground_truth.frame_index,
87+
matches=matches,
88+
ego2map=frame_ground_truth.ego2map,
89+
)

t4_devkit/evaluation/task.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
11
from __future__ import annotations
22

3-
from enum import Enum
3+
from enum import Enum, unique
44

55
__all__ = ["EvaluationTask"]
66

77

8+
@unique
89
class EvaluationTask(str, Enum):
910
"""Enumeration of evaluation tasks."""
1011

1112
DETECTION3D = "detection3d"
1213
TRACKING3D = "tracking3d"
1314
PREDICTION3D = "prediction3d"
15+
SEGMENTATION3D = "segmentation3d"
1416
DETECTION2D = "detection2d"
1517
TRACKING2D = "tracking2d"
18+
SEGMENTATION2D = "segmentation2d"
1619

1720
def is_3d(self) -> bool:
21+
"""Indicate whether the task considers 3D objects.
22+
23+
Returns:
24+
Return `True` if the task is in [detection3d, tracking3d, prediction3d, segmentation3d].
25+
"""
1826
return self in (
1927
EvaluationTask.DETECTION3D,
2028
EvaluationTask.TRACKING3D,
2129
EvaluationTask.PREDICTION3D,
30+
EvaluationTask.SEGMENTATION3D,
2231
)
2332

2433
def is_2d(self) -> bool:
34+
"""Indicate whether the task considers 3D objects.
35+
36+
Returns:
37+
Return `True` if the task is in [detection2d, tracking2d, segmentation2d].
38+
"""
2539
return self in (EvaluationTask.DETECTION2D, EvaluationTask.TRACKING2D)
40+
41+
def is_segmentation(self) -> bool:
42+
"""Indicate whether the task deals with segmentation.
43+
44+
Returns:
45+
Return `True` if the task is in [segmentation3d, segmentation2d].
46+
"""
47+
return self in (EvaluationTask.SEGMENTATION3D, EvaluationTask.SEGMENTATION2D)

tests/evaluation/test_task.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55

66
def test_task() -> None:
7-
task_names = {"detection3d", "tracking3d", "prediction3d", "detection2d", "tracking2d"}
7+
task_names = {
8+
"detection3d",
9+
"tracking3d",
10+
"segmentation3d",
11+
"prediction3d",
12+
"detection2d",
13+
"tracking2d",
14+
"segmentation2d",
15+
}
816

917
assert task_names == {e.value for e in EvaluationTask}
1018

0 commit comments

Comments
 (0)