Formula Student Autonomous Systems
The code for the main driverless system
Loading...
Searching...
No Matches
perception_integration_tests.cpp
Go to the documentation of this file.
1#include <gtest/gtest.h>
2#include <pcl/io/pcd_io.h>
3#include <pcl/point_cloud.h>
4#include <pcl_conversions/pcl_conversions.h>
5
6#include <custom_interfaces/msg/cone_array.hpp>
7#include <custom_interfaces/msg/perception_output.hpp>
8#include <filesystem>
9#include <rclcpp/rclcpp.hpp>
10#include <sensor_msgs/msg/point_cloud2.hpp>
11
13
18bool loadGroundTruth(const std::string& file_path,
19 std::vector<std::tuple<float, float, bool, bool>>& ground_truth) {
20 std::ifstream file(file_path);
21 if (!file.is_open()) return false;
22
23 float x, y;
24 int is_large;
25 while (file >> x >> y >> is_large) {
26 ground_truth.emplace_back(x, y, is_large, false);
27 }
28 return true;
29}
30
35double computeCorrectness(const custom_interfaces::msg::ConeArray::SharedPtr& detected,
36 std::vector<std::tuple<float, float, bool, bool>> ground_truth,
37 float tolerance = 0.2) {
38 int true_positives = 0;
39 int false_positives = 0;
40
41 for (const auto& cone : detected->cone_array) {
42 bool found = false;
43 for (auto& [gt_x, gt_y, gt_is_large, gt_found] : ground_truth) {
44 float distance = std::hypot(cone.position.x - gt_x, cone.position.y - gt_y);
45 if (distance < tolerance && cone.is_large == gt_is_large && !gt_found) {
46 true_positives++;
47 gt_found = true;
48 found = true;
49 break;
50 }
51 }
52 if (!found) false_positives++;
53 }
54
55 // Calculate false negatives
56 int false_negatives = std::count_if(ground_truth.begin(), ground_truth.end(),
57 [](const auto& gt) { return !std::get<3>(gt); });
58
59 // Compute precision and recall
60 double precision =
61 (true_positives == 0) ? 0.0 : (double)true_positives / (true_positives + false_positives);
62 double recall =
63 (true_positives == 0) ? 0.0 : (double)true_positives / (true_positives + false_negatives);
64
65 // Avoid division by zero
66 if (precision == 0.0 && recall == 0.0) return 0.0;
67
68 // Compute F1-score
69 double f1_score = 2.0 * (precision * recall) / (precision + recall);
70 return f1_score * 100.0; // Return as percentage
71}
72
76void generateCylinder(pcl::PointCloud<pcl::PointXYZI>::Ptr cloud, float radius, float height,
77 float centerX, float centerY, int numSlices, int numHeightSegments,
78 float intensity) {
79 for (int i = 0; i <= numHeightSegments; ++i) {
80 float z = i * height / numHeightSegments;
81 for (int j = 0; j < numSlices; ++j) {
82 float theta = 2.0 * M_PI * j / numSlices;
83 float x = centerX + radius * cos(theta);
84 float y = centerY + radius * sin(theta);
85 cloud->points.emplace_back(x, y, z, intensity);
86 }
87 }
88}
89
95class PerceptionIntegrationTest : public ::testing::Test {
96protected:
97 rclcpp::Node::SharedPtr test_node_;
99 rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pcl_publisher_;
100 rclcpp::Subscription<custom_interfaces::msg::PerceptionOutput>::SharedPtr cones_subscriber_;
101 rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr pcl_removed_cloud_subscriber_;
102 custom_interfaces::msg::ConeArray::SharedPtr
104 pcl::PointCloud<pcl::PointXYZI>::Ptr
108
112 void SetUp() override {
113 rclcpp::init(0, nullptr);
114
115 test_node_ = std::make_shared<rclcpp::Node>("test_node");
116
117 // Publisher for input point cloud
118 pcl_publisher_ = test_node_->create_publisher<sensor_msgs::msg::PointCloud2>("/lidar_points",
119 rclcpp::QoS(10));
120
121 // Subscriber for ground removed cloud (msg is shown in the respective results file)
122 pcl_removed_cloud_subscriber_ = test_node_->create_subscription<sensor_msgs::msg::PointCloud2>(
123 "/perception/ground_removed_cloud", 1,
124 [this](const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
125 pcl::fromROSMsg(*msg, *result_cloud_);
126 });
127
128 // Subscriber for output cones
129 cones_subscriber_ = test_node_->create_subscription<custom_interfaces::msg::PerceptionOutput>(
130 "/perception/cones", 1,
131 [this](const custom_interfaces::msg::PerceptionOutput::SharedPtr msg) {
132 cones_result_ = std::make_shared<custom_interfaces::msg::ConeArray>(msg->cones);
133 cones_received_ = true;
134 });
135
136 cones_received_ = false;
137 result_cloud_ = std::make_shared<pcl::PointCloud<pcl::PointXYZI>>();
138 }
139
140 void TearDown() override { rclcpp::shutdown(); }
141
142 void publish_pcd(const std::string& input_pcd_path) {
143 pcl::PointCloud<pcl::PointXYZI> pcl_cloud;
144 if (pcl::io::loadPCDFile<pcl::PointXYZI>(input_pcd_path, pcl_cloud) == -1) {
145 throw std::runtime_error("Could not load PCD file: " + input_pcd_path);
146 }
147
148 // Convert PCL cloud to ROS PointCloud2 message
149 sensor_msgs::msg::PointCloud2 msg;
150 pcl::toROSMsg(pcl_cloud, msg);
151 msg.header.frame_id = "hesai_lidar";
152 msg.header.stamp = test_node_->now();
153
154 // Publish the message
155 pcl_publisher_->publish(msg);
156 }
157
158 bool waitForCones(rclcpp::executors::SingleThreadedExecutor& executor, int timeout_sec = 2) {
159 auto start_time = std::chrono::steady_clock::now();
160 while (!cones_received_ &&
161 std::chrono::steady_clock::now() - start_time < std::chrono::seconds(timeout_sec)) {
162 executor.spin_some();
163 rclcpp::sleep_for(std::chrono::milliseconds(100));
164 }
165 return cones_received_;
166 }
167
168 void writeResults(std::string output_pcd_path, int cone_gt, int large_cone_gt) {
169 if ((int)cones_result_->cone_array.size() != cone_gt)
170 RCLCPP_INFO(test_node_->get_logger(), "Wrong number of cones: detected->%ld expected->%d",
171 cones_result_->cone_array.size(), cone_gt);
172
173 int large_count = 0;
174 for (auto cone : cones_result_->cone_array) {
175 if (cone.is_large) {
176 large_count++;
177 generateCylinder(result_cloud_, 0.220, 0.605, cone.position.x, cone.position.y, 50, 50, 14);
178 } else {
179 generateCylinder(result_cloud_, 0.150, 0.325, cone.position.x, cone.position.y, 50, 50, 36);
180 }
181 }
182
183 if (large_count != large_cone_gt)
184 RCLCPP_INFO(test_node_->get_logger(),
185 "Wrong number of large cones: detected->%d expected->%d", large_count,
186 large_cone_gt);
187 // Number of points in the PCD should be height*width
188 result_cloud_->width = result_cloud_->points.size();
189 result_cloud_->height = 1;
190
191 // Save the generated cloud to a PCD file
192 if (pcl::io::savePCDFile(output_pcd_path, *result_cloud_) == -1) {
193 RCLCPP_ERROR(test_node_->get_logger(), "Failed to save result PCD file.");
194 } else {
195 RCLCPP_INFO(test_node_->get_logger(), "Result cloud saved to: %s", output_pcd_path.c_str());
196 }
197 }
198
199 void runTest(const std::string test_name, const int cone_gt, const int large_cone_gt,
200 const int min_correctness, const uint8_t mission) {
201 auto params = Perception::load_config();
202 params.default_mission_ = mission;
203
204 for (const auto& [mission, trim] : *params.fov_trim_map_) {
205 trim->set_lidar_rotation(90);
206 trim->set_lidar_pitch(-3.9);
207 }
208
209 rclcpp::Node::SharedPtr perception_node = std::make_shared<Perception>(params);
210 ASSERT_NE(perception_node, nullptr) << "Failed to initialize Perception node.";
211
212 rclcpp::executors::SingleThreadedExecutor executor;
213 executor.add_node(perception_node);
214 executor.add_node(test_node_);
215
216 std::string input_pcd_path =
217 "../../src/perception/test/perception_integration_tests/point_clouds/" + test_name + ".pcd";
218 ASSERT_TRUE(std::filesystem::exists(input_pcd_path))
219 << "PCD file does not exist: " << input_pcd_path;
220 std::string output_pcd_path =
221 "../../src/perception/test/perception_integration_tests/results/" + test_name + ".pcd";
222 std::string gt_txt_path =
223 "../../src/perception/test/perception_integration_tests/ground_truths/" + test_name +
224 ".txt";
225 ASSERT_TRUE(std::filesystem::exists(gt_txt_path))
226 << "Ground truth file does not exist: " << gt_txt_path;
227
228 try {
229 publish_pcd(input_pcd_path);
230 } catch (const std::exception& e) {
231 FAIL() << "Failed to publish PCD: " << e.what();
232 }
233
234 ASSERT_TRUE(waitForCones(executor));
235
236 writeResults(output_pcd_path, cone_gt, large_cone_gt);
237
238 std::vector<std::tuple<float, float, bool, bool>> ground_truth;
239 ASSERT_TRUE(loadGroundTruth(gt_txt_path, ground_truth)) << "Failed to load ground truth file.";
240 double correctness = computeCorrectness(cones_result_, ground_truth);
241 RCLCPP_INFO(test_node_->get_logger(), "Correctness score: %.2f%%", correctness);
242 EXPECT_GT(correctness, min_correctness)
243 << "Cone detection correctness below acceptable threshold.";
244
245 executor.cancel();
246 }
247};
248
252TEST_F(PerceptionIntegrationTest, StraightLine) { runTest("straight_line", 20, 4, 80, 1); }
253
258TEST_F(PerceptionIntegrationTest, AccelerationEndClose) {
259 runTest("acceleration_end_close", 12, 4, 80, 1);
260}
261
266TEST_F(PerceptionIntegrationTest, AccelerationEndMedium) {
267 runTest("acceleration_end_medium", 14, 4, 80, 1);
268}
269
275 runTest("acceleration_end_far", 16, 4, 80, 1);
276}
277
281TEST_F(PerceptionIntegrationTest, EnterHairpin) { runTest("enter_hairpin", 16, 0, 70, 4); }
282
286TEST_F(PerceptionIntegrationTest, TurnStart) { runTest("turn_start", 33, 4, 65, 4); }
287
291TEST_F(PerceptionIntegrationTest, OddStituation) { runTest("odd_situation", 33, 4, 70, 4); }
292
296TEST_F(PerceptionIntegrationTest, DiagonalPath) { runTest("diagonal_path", 26, 0, 80, 4); }
static PerceptionParameters load_config()
Turns the parameters in the yaml file into PerceptionParameters class.
Test class for blackbox perception integration tests.
bool waitForCones(rclcpp::executors::SingleThreadedExecutor &executor, int timeout_sec=2)
bool cones_received_
Cloud that will be filled with the found cones as cylinders and saved in the results file for visuali...
void runTest(const std::string test_name, const int cone_gt, const int large_cone_gt, const int min_correctness, const uint8_t mission)
void publish_pcd(const std::string &input_pcd_path)
void SetUp() override
Flag that determines if the cones were recieved on the test node side.
rclcpp::Subscription< sensor_msgs::msg::PointCloud2 >::SharedPtr pcl_removed_cloud_subscriber_
void writeResults(std::string output_pcd_path, int cone_gt, int large_cone_gt)
pcl::PointCloud< pcl::PointXYZI >::Ptr result_cloud_
Recieves and stores perception node output.
rclcpp::Subscription< custom_interfaces::msg::PerceptionOutput >::SharedPtr cones_subscriber_
rclcpp::Publisher< sensor_msgs::msg::PointCloud2 >::SharedPtr pcl_publisher_
Test node created to publish a pcl and subsribe to the cones array on the perception node.
custom_interfaces::msg::ConeArray::SharedPtr cones_result_
std::shared_ptr< rclcpp::executors::SingleThreadedExecutor > executor
bool loadGroundTruth(const std::string &file_path, std::vector< std::tuple< float, float, bool, bool > > &ground_truth)
Helper function to load ground truth cone positions from a file.
double computeCorrectness(const custom_interfaces::msg::ConeArray::SharedPtr &detected, std::vector< std::tuple< float, float, bool, bool > > ground_truth, float tolerance=0.2)
Helper function to compute correctness for a specific test case.
void generateCylinder(pcl::PointCloud< pcl::PointXYZI >::Ptr cloud, float radius, float height, float centerX, float centerY, int numSlices, int numHeightSegments, float intensity)
Generates a cylinder made of pcl points, used to add the found cones in the results pcl.
TEST_F(PerceptionIntegrationTest, StraightLine)
Straight line test for perception node from rosbag: Accelaration_Testing_DV_1B.mcap.