Skip to content

Commit 24ba3d1

Browse files
committed
feat: add CLEAR metric for tracking evaluation (#135)
* feat: add is_matched to check both estimation and gt are not None Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: add CLEAR Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent ad0c62f commit 24ba3d1

3 files changed

Lines changed: 139 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .ap import * # noqa
2+
from .clear import * # noqa
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from attrs import define, field
6+
7+
from ..matching import CenterDistance
8+
9+
if TYPE_CHECKING:
10+
from t4_devkit.evaluation import BoxMatch, FrameBoxMatch, MatchingScorerLike
11+
12+
__all__ = ["Mota", "Motp"]
13+
14+
15+
class Mota:
16+
@define
17+
class ClearBuffer:
18+
num_gt: int = field(init=False, default=0)
19+
num_tp: int = field(init=False, default=0)
20+
num_fp: int = field(init=False, default=0)
21+
num_id_switch: int = field(init=False, default=0)
22+
score: float = field(init=False, default=0.0)
23+
24+
def compute_mota(self) -> float:
25+
return (
26+
max((self.num_tp - self.num_fp - self.num_id_switch) / self.num_gt, 0.0)
27+
if self.num_gt > 0
28+
else 0.0
29+
)
30+
31+
def compute_motp(self) -> float:
32+
return max(self.score / self.num_tp, 0.0) if self.num_tp > 0 else 0.0
33+
34+
def __init__(self, threshold: float) -> None:
35+
self.scorer = self._configure_scorer()
36+
self.threshold = threshold
37+
38+
def _configure_scorer(self) -> MatchingScorerLike:
39+
return CenterDistance()
40+
41+
def __call__(self, frames: list[FrameBoxMatch]) -> float:
42+
buffer = self._compute_clear(frames)
43+
return buffer.compute_mota()
44+
45+
def _compute_clear(self, frames: list[FrameBoxMatch]) -> ClearBuffer:
46+
buffer = self.ClearBuffer()
47+
num_frame = len(frames)
48+
for i in range(1, num_frame):
49+
current_frame = frames[i]
50+
previous_frame = frames[i - 1]
51+
52+
buffer.num_gt += current_frame.num_gt
53+
for current_match in current_frame.matches:
54+
is_id_switch = False
55+
is_same_match = False
56+
for previous_match in previous_frame.matches:
57+
if not previous_match.is_tp(
58+
self.scorer,
59+
self.threshold,
60+
previous_frame.ego2map,
61+
):
62+
continue
63+
64+
is_id_switch = self._is_id_switched(current_match, previous_match)
65+
if is_id_switch:
66+
break
67+
68+
is_same_match = self._is_same_match()
69+
if is_same_match:
70+
buffer.num_tp += current_match.is_tp(
71+
scorer=self.scorer,
72+
threshold=self.threshold,
73+
ego2map=current_frame.ego2map,
74+
)
75+
buffer.score += self.scorer(
76+
previous_match.estimation,
77+
previous_match.ground_truth,
78+
previous_frame.ego2map,
79+
)
80+
break
81+
82+
if is_same_match:
83+
continue
84+
85+
if current_match.is_tp(self.scorer, self.threshold, current_frame.ego2map):
86+
buffer.num_tp += 1
87+
buffer.score += self.scorer(
88+
current_match.estimation,
89+
current_match.ground_truth,
90+
current_frame.ego2map,
91+
)
92+
if is_id_switch:
93+
buffer.num_id_switch += 1
94+
else:
95+
buffer.num_fp += 1
96+
97+
def _is_id_switched(self, current_match: BoxMatch, previous_match: BoxMatch) -> bool:
98+
if (not current_match.is_matched()) and (not previous_match.is_matched()):
99+
return False
100+
101+
is_same_estimated_uuid, is_same_estimated_label, is_same_gt_uuid = self._check_match(
102+
current_match, previous_match
103+
)
104+
105+
if is_same_estimated_uuid and is_same_estimated_label:
106+
return not is_same_gt_uuid
107+
else:
108+
return is_same_gt_uuid
109+
110+
def _is_same_match(self, current_match: BoxMatch, previous_match: BoxMatch) -> bool:
111+
is_same_estimated_uuid, is_same_estimated_label, is_same_gt_uuid = self._check_match(
112+
current_match, previous_match
113+
)
114+
return is_same_estimated_uuid and is_same_estimated_label and is_same_gt_uuid
115+
116+
def _check_match(
117+
self,
118+
current_match: BoxMatch,
119+
previous_match: BoxMatch,
120+
) -> tuple[bool, bool, bool]:
121+
is_same_estimated_uuid = current_match.estimation.uuid == previous_match.estimation.uuid
122+
is_same_estimated_label = (
123+
current_match.estimation.semantic_label == previous_match.estimation.semantic_label
124+
)
125+
is_same_gt_uuid = current_match.ground_truth.uuid == previous_match.ground_truth.uuid
126+
127+
return is_same_estimated_uuid, is_same_estimated_label, is_same_gt_uuid
128+
129+
130+
class Motp(Mota):
131+
def __call__(self, frames: list[FrameBoxMatch]) -> float:
132+
buffer = self._compute_clear(frames)
133+
return buffer.compute_motp()

t4_devkit/evaluation/result/box.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@ def __attrs_post_init__(self) -> None:
3232
if self.estimation is None and self.ground_truth is None:
3333
raise ValueError("At least one of `estimation` or `ground_truth` must be set.")
3434

35+
def is_matched(self) -> bool:
36+
return self.estimation is not None and self.ground_truth is not None
37+
3538
def is_label_ok(self) -> bool:
3639
return (
3740
False
38-
if self.estimation is None or self.ground_truth is None
41+
if not self.is_matched()
3942
else self.estimation.semantic_label == self.ground_truth.semantic_label
4043
)
4144

@@ -45,7 +48,7 @@ def is_tp(
4548
threshold: float,
4649
ego2map: HomogeneousMatrix | None = None,
4750
) -> bool:
48-
if self.estimation is None or self.ground_truth is None:
51+
if not self.is_matched():
4952
return False
5053

5154
score = scorer(self.estimation, self.ground_truth, ego2map)

0 commit comments

Comments
 (0)