From cf027fde2d40001c4aeb3360090e61d880dc2c8a Mon Sep 17 00:00:00 2001 From: sxy0523 <90957654+sxy0523@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:28:42 +0800 Subject: [PATCH] perception: add CenterPoint proposal refine hook --- .../centerpoint_proposal_refinement.md | 112 +++++++ modules/perception/lidar_detection/BUILD | 23 ++ .../center_point_detection.cc | 95 ++++++ .../center_point_detection.h | 3 + .../center_point_detection/proposal_refine.cc | 174 ++++++++++ .../center_point_detection/proposal_refine.h | 101 ++++++ .../proposal_refine_test.cc | 297 ++++++++++++++++++ .../proto/model_param.proto | 13 +- 8 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 docs/06_Perception/centerpoint_proposal_refinement.md create mode 100644 modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.cc create mode 100644 modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h create mode 100644 modules/perception/lidar_detection/detector/center_point_detection/proposal_refine_test.cc diff --git a/docs/06_Perception/centerpoint_proposal_refinement.md b/docs/06_Perception/centerpoint_proposal_refinement.md new file mode 100644 index 00000000000..3c766a708d6 --- /dev/null +++ b/docs/06_Perception/centerpoint_proposal_refinement.md @@ -0,0 +1,112 @@ +# CenterPoint Proposal Refinement Extension + +This document describes an optional CenterPoint proposal refinement extension +point for LiDAR detection. The extension is disabled by default and preserves +the original CenterPoint behavior unless explicitly enabled in the detector +model config. + +## Scope + +The extension point runs after CenterPoint proposal decode and before the +existing object construction and NMS/filtering path. + +```text +CenterPoint preprocess / inference / decode + -> optional proposal refinement + -> existing object construction / NMS / filtering + -> existing perception output +``` + +It does not change Cyber RT input or output channels, perception message +semantics, tracking, fusion, prediction, or planning. + +## Configuration + +The extension is controlled by `ProposalRefineParam` in +`modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.proto`. + +Important fields: + +```text +enable_proposal_refine: false +proposal_refine_mode: "mock_zero" +proposal_refine_top_k: -1 +proposal_refine_score_threshold: 0.0 +proposal_refine_delta_scale: 1.0 +proposal_refine_debug_log: false +strict_refine: true +``` + +When `enable_proposal_refine=false`, the extension does not run and the original +CenterPoint path is preserved. + +## Modes + +`mock_zero` is a no-op mode. It selects proposals, records statistics, and +leaves boxes, scores, and labels unchanged. + +`mock_delta` applies a deterministic small delta for test coverage: + +```text +delta_x = 0.1 +delta_yaw = 0.05 +delta_score = 0.01 +``` + +Both modes are intended for validating the extension point, configuration +gating, delta application, and rollback behavior. They are not intended to claim +perception quality improvement. + +## Safety + +- Disabled by default. +- No new runtime dependency on quantum libraries. +- No Cyber RT channel changes. +- No public perception message changes. +- No tracking/fusion/prediction/planning changes. +- Box dimensions are clamped positive after delta application. +- Yaw is normalized. +- Score is clamped to `[0, 1]`. + +## Rollback + +Set: + +```text +enable_proposal_refine: false +``` + +or remove the optional `proposal_refine` config block. With refinement disabled, +the module skips proposal conversion/refinement and continues through the +original CenterPoint detection path. + +## Tests + +Focused tests: + +```bash +bazel test --config=unit_test --cache_test_results=no --test_output=errors \ + //modules/perception/lidar_detection:proposal_refine_test +``` + +Focused build: + +```bash +bazel build --config=opt --config=gpu --config=nvidia \ + //modules/perception/lidar_detection:apollo_perception_lidar_detection \ + //modules/perception/lidar_detection:liblidar_detection_component.so +``` + +Lint targets: + +```bash +bazel build \ + //modules/perception/lidar_detection:center_point_proposal_refine_cpplint \ + //modules/perception/lidar_detection:proposal_refine_test_cpplint +``` + +## Follow-Up Work + +Optional proposal export, offline teacher experiments, student-model inference, +and experiment reports should be split into later PRs. They are intentionally +outside the conservative first PR scope. diff --git a/modules/perception/lidar_detection/BUILD b/modules/perception/lidar_detection/BUILD index 5acc48c2786..65e451275f4 100644 --- a/modules/perception/lidar_detection/BUILD +++ b/modules/perception/lidar_detection/BUILD @@ -17,6 +17,17 @@ filegroup( ]), ) +apollo_cc_library( + name = "center_point_proposal_refine", + srcs = ["detector/center_point_detection/proposal_refine.cc"], + hdrs = ["detector/center_point_detection/proposal_refine.h"], + deps = [ + "//cyber", + "//modules/common/math", + "//modules/perception/lidar_detection/detector/center_point_detection/proto:center_point_model_param_cc_proto", + ], +) + apollo_cc_library( name = "point_pillars_common", hdrs = ["detector/point_pillars_detection/common.h"], @@ -163,6 +174,7 @@ apollo_cc_library( copts = PERCEPTION_COPTS + if_profiler() + ["-DENABLE_PROFILER=1"], deps = [ ":anchor_mask_cuda", + ":center_point_proposal_refine", ":feature_generator_cuda", ":nms_cuda", ":pfe_cuda", @@ -220,6 +232,17 @@ apollo_component( ], ) +apollo_cc_test( + name = "proposal_refine_test", + size = "small", + srcs = ["detector/center_point_detection/proposal_refine_test.cc"], + deps = [ + ":center_point_proposal_refine", + "//modules/common/math", + "@com_google_googletest//:gtest_main", + ], +) + apollo_cc_test( name = "cnn_segmentation_test", size = "large", diff --git a/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.cc b/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.cc index 3c3bfee26cd..9e6237dd81d 100644 --- a/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.cc +++ b/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.cc @@ -52,6 +52,53 @@ using base::Object; using base::PointD; using base::PointF; +namespace { + +std::vector BuildProposals(const std::vector &detections, + const std::vector &labels, + const std::vector &scores, + int num_output_box_feature) { + std::vector proposals; + proposals.reserve(scores.size()); + for (size_t i = 0; i < scores.size(); ++i) { + const size_t offset = i * num_output_box_feature; + Proposal proposal; + proposal.x = detections[offset + 0]; + proposal.y = detections[offset + 1]; + proposal.z = detections[offset + 2]; + proposal.width = detections[offset + 3]; + proposal.length = detections[offset + 4]; + proposal.height = detections[offset + 5]; + proposal.yaw = detections[offset + 6]; + proposal.score = scores[i]; + proposal.class_id = static_cast(labels[i]); + proposals.push_back(proposal); + } + return proposals; +} + +void WriteProposals(const std::vector &proposals, + int num_output_box_feature, + std::vector *detections, + std::vector *labels, + std::vector *scores) { + for (size_t i = 0; i < proposals.size(); ++i) { + const size_t offset = i * num_output_box_feature; + const Proposal &proposal = proposals[i]; + (*detections)[offset + 0] = proposal.x; + (*detections)[offset + 1] = proposal.y; + (*detections)[offset + 2] = proposal.z; + (*detections)[offset + 3] = proposal.width; + (*detections)[offset + 4] = proposal.length; + (*detections)[offset + 5] = proposal.height; + (*detections)[offset + 6] = proposal.yaw; + (*scores)[i] = proposal.score; + (*labels)[i] = proposal.class_id; + } +} + +} // namespace + // point cloud range CenterPointDetection::CenterPointDetection() : x_min_range_(Params::kMinXRange), @@ -98,6 +145,12 @@ bool CenterPointDetection::Init(const LidarDetectorInitOptions &options) { diff_class_iou_ = model_param_.diff_class_iou(); diff_class_nms_ = model_param_.diff_class_nms(); + if (!proposal_refine_module_.Init(model_param_.proposal_refine())) { + AERROR << "Failed to init proposal refine module. mode: " + << model_param_.proposal_refine().proposal_refine_mode(); + return false; + } + cone_score_threshold_ = model_param_.postprocess().cone_score_threshold(); ped_score_threshold_ = model_param_.postprocess().ped_score_threshold(); cyc_score_threshold_ = model_param_.postprocess().cyc_score_threshold(); @@ -330,6 +383,48 @@ bool CenterPointDetection::Detect(const LidarDetectorOptions &options, FilterDiffScore(output_bbox_blob, output_label_blob, output_score_blob, &out_detections, &out_labels, &out_scores); + if (proposal_refine_module_.enabled()) { + const int num_output_box_feature = + model_param_.postprocess().num_output_box_feature(); + if (num_output_box_feature < 7) { + AERROR << "Invalid num_output_box_feature: " << num_output_box_feature; + return false; + } + if (out_labels.size() != out_scores.size() || + out_detections.size() != out_scores.size() * num_output_box_feature) { + AERROR << "Proposal input size mismatch. detections: " + << out_detections.size() << " labels: " << out_labels.size() + << " scores: " << out_scores.size() + << " num_output_box_feature: " << num_output_box_feature; + return false; + } + + std::vector proposals = BuildProposals( + out_detections, out_labels, out_scores, num_output_box_feature); + ProposalRefineStats refine_stats; + if (!MaybeRefineProposals(&proposal_refine_module_, &proposals, + &refine_stats)) { + AERROR << "Proposal refine failed."; + if (proposal_refine_module_.strict_refine()) { + return false; + } + } else { + WriteProposals(proposals, num_output_box_feature, &out_detections, + &out_labels, &out_scores); + } + if (proposal_refine_module_.debug_log()) { + AINFO << "Proposal refine enabled: true" + << " mode: " << proposal_refine_module_.mode() + << " proposal_count_before_refine: " + << refine_stats.proposal_count + << " refined_proposal_count: " + << refine_stats.refined_proposal_count + << " delta_min: " << refine_stats.delta_min + << " delta_max: " << refine_stats.delta_max + << " latency_ms: " << refine_stats.latency_ms; + } + } + GetObjects(frame->lidar2world_pose, out_detections, out_labels, out_scores, &frame->segmented_objects); diff --git a/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.h b/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.h index 5182793f7bb..655dcd8b06e 100644 --- a/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.h +++ b/modules/perception/lidar_detection/detector/center_point_detection/center_point_detection.h @@ -32,6 +32,7 @@ #include "modules/perception/common/lidar/common/lidar_frame.h" #include "modules/perception/lidar_detection/interface/base_lidar_detector.h" #include "modules/perception/common/interface/base_down_sample.h" +#include "modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h" #include "modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.pb.h" namespace apollo { @@ -200,6 +201,8 @@ class CenterPointDetection : public BaseLidarDetector { std::vector output_blob_names_; std::shared_ptr down_sample_; + + ProposalRefineModule proposal_refine_module_; }; // class CenterPointDetection } // namespace lidar diff --git a/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.cc b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.cc new file mode 100644 index 00000000000..07c28d45313 --- /dev/null +++ b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.cc @@ -0,0 +1,174 @@ +/****************************************************************************** + * Copyright 2026 The Apollo Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#include "modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h" + +#include +#include +#include + +#include "cyber/common/log.h" +#include "modules/common/math/math_utils.h" + +namespace apollo { +namespace perception { +namespace lidar { +namespace { + +constexpr char kMockZeroMode[] = "mock_zero"; +constexpr char kMockDeltaMode[] = "mock_delta"; +constexpr float kMinBoxDimension = 1.0e-3f; + +} // namespace + +bool ProposalRefineModule::Init( + const centerpoint::ProposalRefineParam& config) { + enabled_ = config.enable_proposal_refine(); + mode_ = config.proposal_refine_mode(); + if (mode_.empty()) { + mode_ = kMockZeroMode; + } + top_k_ = config.proposal_refine_top_k(); + score_threshold_ = config.proposal_refine_score_threshold(); + delta_scale_ = config.proposal_refine_delta_scale(); + debug_log_ = config.proposal_refine_debug_log(); + strict_refine_ = config.strict_refine(); + refine_call_count_ = 0; + + if (!enabled_) { + return true; + } + if (top_k_ < -1) { + AERROR << "proposal_refine_top_k must be -1 or non-negative."; + return false; + } + if (mode_ != kMockZeroMode && mode_ != kMockDeltaMode) { + AERROR << "Unsupported proposal refine mode: " << mode_; + return false; + } + return true; +} + +bool ProposalRefineModule::Refine(std::vector* proposals, + ProposalRefineStats* stats) { + if (proposals == nullptr || stats == nullptr) { + return false; + } + ++refine_call_count_; + + const auto start = std::chrono::steady_clock::now(); + stats->proposal_count = proposals->size(); + const std::vector selected_indices = + SelectProposalIndices(*proposals); + stats->refined_proposal_count = selected_indices.size(); + stats->delta_min = 0.0f; + stats->delta_max = 0.0f; + + if (mode_ == kMockDeltaMode) { + if (!selected_indices.empty()) { + const ProposalDelta delta = GetMockDelta(); + stats->delta_min = std::numeric_limits::max(); + stats->delta_max = std::numeric_limits::lowest(); + const float raw_deltas[] = { + delta.delta_x, delta.delta_y, delta.delta_z, + delta.delta_length, delta.delta_width, delta.delta_height, + delta.delta_yaw, delta.delta_score, + }; + for (const float value : raw_deltas) { + stats->delta_min = std::min(stats->delta_min, value); + stats->delta_max = std::max(stats->delta_max, value); + } + for (const size_t index : selected_indices) { + ApplyProposalDelta(delta, delta_scale_, &proposals->at(index)); + } + } + } else if (mode_ != kMockZeroMode) { + return false; + } + + const auto end = std::chrono::steady_clock::now(); + stats->latency_ms = + std::chrono::duration(end - start).count(); + return true; +} + +std::vector ProposalRefineModule::SelectProposalIndices( + const std::vector& proposals) const { + std::vector selected_indices; + selected_indices.reserve(proposals.size()); + for (size_t i = 0; i < proposals.size(); ++i) { + if (proposals[i].score >= score_threshold_) { + selected_indices.push_back(i); + } + } + + std::stable_sort(selected_indices.begin(), selected_indices.end(), + [&proposals](size_t lhs, size_t rhs) { + return proposals[lhs].score > proposals[rhs].score; + }); + + if (top_k_ >= 0 && selected_indices.size() > static_cast(top_k_)) { + selected_indices.resize(static_cast(top_k_)); + } + return selected_indices; +} + +ProposalDelta GetMockDelta() { + ProposalDelta delta; + delta.delta_x = 0.1f; + delta.delta_yaw = 0.05f; + delta.delta_score = 0.01f; + return delta; +} + +void ApplyProposalDelta(const ProposalDelta& delta, float delta_scale, + Proposal* proposal) { + if (proposal == nullptr) { + return; + } + proposal->x += delta_scale * delta.delta_x; + proposal->y += delta_scale * delta.delta_y; + proposal->z += delta_scale * delta.delta_z; + proposal->length = std::max( + kMinBoxDimension, proposal->length + delta_scale * delta.delta_length); + proposal->width = std::max(kMinBoxDimension, + proposal->width + delta_scale * delta.delta_width); + proposal->height = std::max( + kMinBoxDimension, proposal->height + delta_scale * delta.delta_height); + proposal->yaw = static_cast(apollo::common::math::NormalizeAngle( + proposal->yaw + delta_scale * delta.delta_yaw)); + proposal->score = apollo::common::math::Clamp( + proposal->score + delta_scale * delta.delta_score, 0.0f, 1.0f); +} + +bool MaybeRefineProposals(ProposalRefineModule* module, + std::vector* proposals, + ProposalRefineStats* stats) { + if (module == nullptr) { + return false; + } + if (!module->enabled()) { + if (stats != nullptr) { + *stats = ProposalRefineStats(); + } + return true; + } + return module->Refine(proposals, stats); +} + +} // namespace lidar +} // namespace perception +} // namespace apollo diff --git a/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h new file mode 100644 index 00000000000..cbb8b95b9d6 --- /dev/null +++ b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h @@ -0,0 +1,101 @@ +/****************************************************************************** + * Copyright 2026 The Apollo Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +#include "modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.pb.h" + +namespace apollo { +namespace perception { +namespace lidar { + +struct Proposal { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float length = 0.0f; + float width = 0.0f; + float height = 0.0f; + float yaw = 0.0f; + float score = 0.0f; + int class_id = 0; +}; + +struct ProposalDelta { + float delta_x = 0.0f; + float delta_y = 0.0f; + float delta_z = 0.0f; + float delta_length = 0.0f; + float delta_width = 0.0f; + float delta_height = 0.0f; + float delta_yaw = 0.0f; + float delta_score = 0.0f; +}; + +struct ProposalRefineStats { + size_t proposal_count = 0; + size_t refined_proposal_count = 0; + float delta_min = 0.0f; + float delta_max = 0.0f; + double latency_ms = 0.0; +}; + +class ProposalRefineModule { + public: + ProposalRefineModule() = default; + ~ProposalRefineModule() = default; + ProposalRefineModule(const ProposalRefineModule&) = delete; + ProposalRefineModule& operator=(const ProposalRefineModule&) = delete; + + bool Init(const centerpoint::ProposalRefineParam& config); + + bool enabled() const { return enabled_; } + bool debug_log() const { return debug_log_; } + bool strict_refine() const { return strict_refine_; } + const std::string& mode() const { return mode_; } + int refine_call_count() const { return refine_call_count_; } + + bool Refine(std::vector* proposals, ProposalRefineStats* stats); + + private: + std::vector SelectProposalIndices( + const std::vector& proposals) const; + + bool enabled_ = false; + std::string mode_ = "mock_zero"; + int top_k_ = -1; + float score_threshold_ = 0.0f; + float delta_scale_ = 1.0f; + bool debug_log_ = false; + bool strict_refine_ = true; + int refine_call_count_ = 0; +}; + +ProposalDelta GetMockDelta(); +void ApplyProposalDelta(const ProposalDelta& delta, float delta_scale, + Proposal* proposal); + +bool MaybeRefineProposals(ProposalRefineModule* module, + std::vector* proposals, + ProposalRefineStats* stats); + +} // namespace lidar +} // namespace perception +} // namespace apollo diff --git a/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine_test.cc b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine_test.cc new file mode 100644 index 00000000000..e0bb6e3c0f5 --- /dev/null +++ b/modules/perception/lidar_detection/detector/center_point_detection/proposal_refine_test.cc @@ -0,0 +1,297 @@ +/****************************************************************************** + * Copyright 2026 The Apollo Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#include "modules/perception/lidar_detection/detector/center_point_detection/proposal_refine.h" + +#include +#include + +#include "gtest/gtest.h" + +#include "modules/common/math/math_utils.h" + +namespace apollo { +namespace perception { +namespace lidar { +namespace { + +constexpr double kPi = 3.14159265358979323846; + +Proposal MakeProposal(float x, float score, int class_id = 0) { + Proposal proposal; + proposal.x = x; + proposal.y = x + 1.0f; + proposal.z = x + 2.0f; + proposal.length = x + 3.0f; + proposal.width = x + 4.0f; + proposal.height = x + 5.0f; + proposal.yaw = x + 6.0f; + proposal.score = score; + proposal.class_id = class_id; + return proposal; +} + +void ExpectProposalEq(const Proposal& lhs, const Proposal& rhs) { + EXPECT_FLOAT_EQ(lhs.x, rhs.x); + EXPECT_FLOAT_EQ(lhs.y, rhs.y); + EXPECT_FLOAT_EQ(lhs.z, rhs.z); + EXPECT_FLOAT_EQ(lhs.length, rhs.length); + EXPECT_FLOAT_EQ(lhs.width, rhs.width); + EXPECT_FLOAT_EQ(lhs.height, rhs.height); + EXPECT_FLOAT_EQ(lhs.yaw, rhs.yaw); + EXPECT_FLOAT_EQ(lhs.score, rhs.score); + EXPECT_EQ(lhs.class_id, rhs.class_id); +} + +} // namespace + +TEST(ProposalRefineMathTest, NormalizeAngleBoundaries) { + EXPECT_DOUBLE_EQ(-kPi, apollo::common::math::NormalizeAngle(kPi)); + EXPECT_DOUBLE_EQ(-kPi, apollo::common::math::NormalizeAngle(-kPi)); + EXPECT_NEAR(0.25, apollo::common::math::NormalizeAngle(4.0 * kPi + 0.25), + 1.0e-12); +} + +TEST(ProposalRefineMathTest, ClampScore) { + EXPECT_FLOAT_EQ(0.0f, apollo::common::math::Clamp(-0.2f, 0.0f, 1.0f)); + EXPECT_FLOAT_EQ(0.4f, apollo::common::math::Clamp(0.4f, 0.0f, 1.0f)); + EXPECT_FLOAT_EQ(1.0f, apollo::common::math::Clamp(1.2f, 0.0f, 1.0f)); +} + +TEST(ProposalRefineMathTest, PositiveDimensions) { + Proposal proposal = MakeProposal(1.0f, 0.5f); + proposal.length = 0.2f; + proposal.width = 0.3f; + proposal.height = 0.4f; + ProposalDelta delta; + delta.delta_length = -10.0f; + delta.delta_width = -10.0f; + delta.delta_height = -10.0f; + + ApplyProposalDelta(delta, 1.0f, &proposal); + + EXPECT_GT(proposal.length, 0.0f); + EXPECT_GT(proposal.width, 0.0f); + EXPECT_GT(proposal.height, 0.0f); +} + +TEST(ProposalRefineModuleTest, DisabledPathDoesNotCallRefine) { + centerpoint::ProposalRefineParam config; + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + ASSERT_FALSE(module.enabled()); + + std::vector proposals = {MakeProposal(1.0f, 0.9f)}; + const std::vector original = proposals; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + + EXPECT_EQ(0, module.refine_call_count()); + EXPECT_EQ(0U, stats.proposal_count); + EXPECT_EQ(0U, stats.refined_proposal_count); + ASSERT_EQ(original.size(), proposals.size()); + ExpectProposalEq(original[0], proposals[0]); +} + +TEST(ProposalRefineModuleTest, MockZeroLeavesProposalsUnchanged) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_zero"); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = { + MakeProposal(1.0f, 0.7f, 0), + MakeProposal(2.0f, 0.8f, 1), + }; + const std::vector original = proposals; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + + EXPECT_EQ(1, module.refine_call_count()); + EXPECT_EQ(2U, stats.proposal_count); + EXPECT_EQ(2U, stats.refined_proposal_count); + EXPECT_FLOAT_EQ(0.0f, stats.delta_min); + EXPECT_FLOAT_EQ(0.0f, stats.delta_max); + ASSERT_EQ(original.size(), proposals.size()); + for (size_t i = 0; i < proposals.size(); ++i) { + ExpectProposalEq(original[i], proposals[i]); + } +} + +TEST(ProposalRefineModuleTest, MockDeltaChangesProposalsDeterministically) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_delta"); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = {MakeProposal(1.0f, 0.7f, 0)}; + proposals[0].yaw = 0.2f; + const Proposal original = proposals[0]; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + + EXPECT_EQ(1, module.refine_call_count()); + EXPECT_EQ(1U, stats.proposal_count); + EXPECT_EQ(1U, stats.refined_proposal_count); + EXPECT_FLOAT_EQ(0.0f, stats.delta_min); + EXPECT_FLOAT_EQ(0.1f, stats.delta_max); + EXPECT_FLOAT_EQ(original.x + 0.1f, proposals[0].x); + EXPECT_FLOAT_EQ(original.y, proposals[0].y); + EXPECT_FLOAT_EQ(original.z, proposals[0].z); + EXPECT_FLOAT_EQ(original.length, proposals[0].length); + EXPECT_FLOAT_EQ(original.width, proposals[0].width); + EXPECT_FLOAT_EQ(original.height, proposals[0].height); + EXPECT_NEAR(original.yaw + 0.05f, proposals[0].yaw, 1.0e-6f); + EXPECT_FLOAT_EQ(original.score + 0.01f, proposals[0].score); +} + +TEST(ProposalRefineModuleTest, MockDeltaWrapsYawAndClampsScore) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_delta"); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = {MakeProposal(1.0f, 0.995f, 0)}; + proposals[0].yaw = static_cast(kPi - 0.02); + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + + const double expected_yaw = + apollo::common::math::NormalizeAngle(kPi - 0.02 + 0.05); + EXPECT_NEAR(expected_yaw, proposals[0].yaw, 1.0e-6); + EXPECT_FLOAT_EQ(1.0f, proposals[0].score); +} + +TEST(ProposalRefineModuleTest, MockDeltaRespectsThresholdAndTopK) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_delta"); + config.set_proposal_refine_score_threshold(0.5f); + config.set_proposal_refine_top_k(1); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = { + MakeProposal(1.0f, 0.9f), + MakeProposal(2.0f, 0.8f), + MakeProposal(3.0f, 0.4f), + }; + const std::vector original = proposals; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + + EXPECT_EQ(3U, stats.proposal_count); + EXPECT_EQ(1U, stats.refined_proposal_count); + EXPECT_FLOAT_EQ(original[0].x + 0.1f, proposals[0].x); + ExpectProposalEq(original[1], proposals[1]); + ExpectProposalEq(original[2], proposals[2]); +} + +TEST(ProposalRefineModuleTest, EmptyProposalsDoNotCrash) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_zero"); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + EXPECT_EQ(1, module.refine_call_count()); + EXPECT_EQ(0U, stats.proposal_count); + EXPECT_EQ(0U, stats.refined_proposal_count); + EXPECT_TRUE(proposals.empty()); +} + +TEST(ProposalRefineModuleTest, EmptyMockDeltaProposalsDoNotCrash) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_delta"); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + EXPECT_EQ(1, module.refine_call_count()); + EXPECT_EQ(0U, stats.proposal_count); + EXPECT_EQ(0U, stats.refined_proposal_count); + EXPECT_TRUE(proposals.empty()); +} + +TEST(ProposalRefineModuleTest, ThresholdFilteringControlsRefineCount) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_zero"); + config.set_proposal_refine_score_threshold(0.5f); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = { + MakeProposal(1.0f, 0.49f), + MakeProposal(2.0f, 0.5f), + MakeProposal(3.0f, 0.9f), + }; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + EXPECT_EQ(3U, stats.proposal_count); + EXPECT_EQ(2U, stats.refined_proposal_count); +} + +TEST(ProposalRefineModuleTest, TopKLimitsRefineCount) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("mock_zero"); + config.set_proposal_refine_top_k(2); + + ProposalRefineModule module; + ASSERT_TRUE(module.Init(config)); + + std::vector proposals = { + MakeProposal(1.0f, 0.1f), + MakeProposal(2.0f, 0.9f), + MakeProposal(3.0f, 0.8f), + MakeProposal(4.0f, 0.7f), + }; + ProposalRefineStats stats; + ASSERT_TRUE(MaybeRefineProposals(&module, &proposals, &stats)); + EXPECT_EQ(4U, stats.proposal_count); + EXPECT_EQ(2U, stats.refined_proposal_count); +} + +TEST(ProposalRefineModuleTest, UnsupportedEnabledModeFailsInit) { + centerpoint::ProposalRefineParam config; + config.set_enable_proposal_refine(true); + config.set_proposal_refine_mode("unknown_mode"); + + ProposalRefineModule module; + EXPECT_FALSE(module.Init(config)); +} + +} // namespace lidar +} // namespace perception +} // namespace apollo diff --git a/modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.proto b/modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.proto index e84cfc1bbed..e653c9d624d 100644 --- a/modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.proto +++ b/modules/perception/lidar_detection/detector/center_point_detection/proto/model_param.proto @@ -6,6 +6,16 @@ import "modules/perception/common/proto/model_info.proto"; import "modules/perception/common/proto/model_process.proto"; import "modules/perception/common/proto/plugin_param.proto"; +message ProposalRefineParam { + optional bool enable_proposal_refine = 1 [default = false]; + optional string proposal_refine_mode = 2 [default = "mock_zero"]; + optional int32 proposal_refine_top_k = 3 [default = -1]; + optional float proposal_refine_score_threshold = 4 [default = 0.0]; + optional float proposal_refine_delta_scale = 5 [default = 1.0]; + optional bool proposal_refine_debug_log = 6 [default = false]; + optional bool strict_refine = 7 [default = true]; +} + message ModelParam { optional common.ModelInfo info = 1; optional common.PointCloudPreProcess preprocess = 2; @@ -21,4 +31,5 @@ message ModelParam { optional bool filter_by_semantic_type = 12 [default = false]; optional bool filter_ground_points = 13 [default = false]; optional PluginParam plugins = 14; -} \ No newline at end of file + optional ProposalRefineParam proposal_refine = 15; +}