From a101549d7af7a297e6b7b0748c6cf620fe9fee8b Mon Sep 17 00:00:00 2001 From: vividf Date: Wed, 5 Aug 2026 12:48:57 +0900 Subject: [PATCH] fix(t4metric_v2): filter GT by annotation num_lidar_pts instead of recomputing from input points The min_num_points GT filter recomputed per-box point counts with points_in_rbbox on the model input point cloud (multi-sweep, after remove_close and range filtering). This couples the evaluation GT set to the input pipeline configuration: changing the sweep count or any point-cloud preprocessing changes which GT boxes are evaluated, so runs with different input configs are not comparable. Sweep accumulation is also not object-motion compensated, so the recomputed counts are physically wrong for moving objects. Use the annotation num_lidar_pts stored in the info pkl instead (keyframe count, the nuScenes/Waymo convention), which keeps the GT set a fixed property of the dataset. Verified on the j6gen2_base val split (3645 frames): the field is fully populated, and the filtered GT set now matches an independent implementation class by class. Signed-off-by: vividf --- .../evaluation/t4metric/t4metric_v2.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/autoware_ml/detection3d/evaluation/t4metric/t4metric_v2.py b/autoware_ml/detection3d/evaluation/t4metric/t4metric_v2.py index ffc5fee4c..74dbd553e 100644 --- a/autoware_ml/detection3d/evaluation/t4metric/t4metric_v2.py +++ b/autoware_ml/detection3d/evaluation/t4metric/t4metric_v2.py @@ -11,7 +11,6 @@ import torch from mmdet3d.registry import METRICS from mmdet3d.structures import LiDARInstance3DBoxes -from mmdet3d.structures.ops import box_np_ops from mmengine.dist import get_world_size from mmengine.evaluator import BaseMetric from mmengine.logging import MessageHub, MMLogger @@ -438,11 +437,10 @@ def process(self, data_batch: dict, data_samples: Sequence[dict]) -> None: # Skip processing if result pickle already exists return - batch_points = data_batch["inputs"]["points"] - for data_sample, points in zip(data_samples, batch_points): + for data_sample in data_samples: current_time = data_sample["timestamp"] scene_id = self._parse_scene_id(data_sample["lidar_path"]) - frame_ground_truth = self._parse_ground_truth_from_sample(current_time, data_sample, points) + frame_ground_truth = self._parse_ground_truth_from_sample(current_time, data_sample) perception_frame = self._parse_predictions_from_sample(current_time, data_sample, frame_ground_truth) self._save_perception_frame(scene_id, data_sample["sample_idx"], perception_frame) @@ -1384,7 +1382,7 @@ def _parse_scene_id(self, lidar_path: str) -> str: except ValueError: return _UNKNOWN - def _parse_ground_truth_from_sample(self, time: float, data_sample: Dict[str, Any], points) -> FrameGroundTruth: + def _parse_ground_truth_from_sample(self, time: float, data_sample: Dict[str, Any]) -> FrameGroundTruth: """Parses ground truth objects from the given data sample. Args: @@ -1415,10 +1413,10 @@ def _parse_ground_truth_from_sample(self, time: float, data_sample: Dict[str, An num_lidar_pts: np.ndarray = eval_info.get("num_lidar_pts", np.array([])) if self.min_num_points > 0 and len(bboxes): - points_cpu = points.cpu().numpy() - indices = box_np_ops.points_in_rbbox(points_cpu[:, :3], bboxes[:, :7]) - num_points_in_gt = indices.sum(0) - bboxes_mask = num_points_in_gt >= self.min_num_points + # Filter by the annotation's keyframe point count (nuScenes/Waymo convention) + # so the evaluation GT set stays independent of the model input pipeline + # (sweep count, remove_close, range filter). + bboxes_mask = num_lidar_pts >= self.min_num_points bboxes = bboxes[bboxes_mask] gt_labels_3d = gt_labels_3d[bboxes_mask] num_lidar_pts = num_lidar_pts[bboxes_mask]