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>
6#include <custom_interfaces/msg/cone_array.hpp>
7#include <custom_interfaces/msg/perception_output.hpp>
9#include <rclcpp/rclcpp.hpp>
10#include <sensor_msgs/msg/point_cloud2.hpp>
19 std::vector<std::tuple<float, float, bool, bool>>& ground_truth) {
20 std::ifstream file(file_path);
21 if (!file.is_open())
return false;
25 while (file >> x >> y >> is_large) {
26 ground_truth.emplace_back(x, y, is_large,
false);
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;
41 for (
const auto& cone : detected->cone_array) {
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) {
52 if (!found) false_positives++;
56 int false_negatives = std::count_if(ground_truth.begin(), ground_truth.end(),
57 [](
const auto& gt) { return !std::get<3>(gt); });
61 (true_positives == 0) ? 0.0 : (double)true_positives / (true_positives + false_positives);
63 (true_positives == 0) ? 0.0 : (double)true_positives / (true_positives + false_negatives);
66 if (precision == 0.0 && recall == 0.0)
return 0.0;
69 double f1_score = 2.0 * (precision * recall) / (precision + recall);
70 return f1_score * 100.0;
76void generateCylinder(pcl::PointCloud<pcl::PointXYZI>::Ptr cloud,
float radius,
float height,
77 float centerX,
float centerY,
int numSlices,
int numHeightSegments,
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);
102 custom_interfaces::msg::ConeArray::SharedPtr
104 pcl::PointCloud<pcl::PointXYZI>::Ptr
113 rclcpp::init(0,
nullptr);
115 test_node_ = std::make_shared<rclcpp::Node>(
"test_node");
123 "/perception/ground_removed_cloud", 1,
124 [
this](
const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
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);
137 result_cloud_ = std::make_shared<pcl::PointCloud<pcl::PointXYZI>>();
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);
149 sensor_msgs::msg::PointCloud2 msg;
150 pcl::toROSMsg(pcl_cloud, msg);
151 msg.header.frame_id =
"hesai_lidar";
159 auto start_time = std::chrono::steady_clock::now();
161 std::chrono::steady_clock::now() - start_time < std::chrono::seconds(timeout_sec)) {
163 rclcpp::sleep_for(std::chrono::milliseconds(100));
168 void writeResults(std::string output_pcd_path,
int cone_gt,
int large_cone_gt) {
170 RCLCPP_INFO(
test_node_->get_logger(),
"Wrong number of cones: detected->%ld expected->%d",
183 if (large_count != large_cone_gt)
185 "Wrong number of large cones: detected->%d expected->%d", large_count,
192 if (pcl::io::savePCDFile(output_pcd_path, *
result_cloud_) == -1) {
193 RCLCPP_ERROR(
test_node_->get_logger(),
"Failed to save result PCD file.");
195 RCLCPP_INFO(
test_node_->get_logger(),
"Result cloud saved to: %s", output_pcd_path.c_str());
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) {
202 params.default_mission_ = mission;
204 for (
const auto& [mission, trim] : *params.fov_trim_map_) {
205 trim->set_lidar_rotation(90);
206 trim->set_lidar_pitch(-3.9);
209 rclcpp::Node::SharedPtr perception_node = std::make_shared<Perception>(params);
210 ASSERT_NE(perception_node,
nullptr) <<
"Failed to initialize Perception node.";
212 rclcpp::executors::SingleThreadedExecutor
executor;
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 +
225 ASSERT_TRUE(std::filesystem::exists(gt_txt_path))
226 <<
"Ground truth file does not exist: " << gt_txt_path;
230 }
catch (
const std::exception& e) {
231 FAIL() <<
"Failed to publish PCD: " << e.what();
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.";
241 RCLCPP_INFO(
test_node_->get_logger(),
"Correctness score: %.2f%%", correctness);
242 EXPECT_GT(correctness, min_correctness)
243 <<
"Cone detection correctness below acceptable threshold.";
259 runTest(
"acceleration_end_close", 12, 4, 80, 1);
267 runTest(
"acceleration_end_medium", 14, 4, 80, 1);
275 runTest(
"acceleration_end_far", 16, 4, 80, 1);
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::Node::SharedPtr test_node_
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.