add humble-navigation2

This commit is contained in:
X-lanni
2025-05-27 19:03:40 +08:00
parent 974abb5e1e
commit e74ec539c2
1280 changed files with 204114 additions and 0 deletions
@@ -0,0 +1,58 @@
ament_add_gtest(array_parser_test array_parser_test.cpp)
target_link_libraries(array_parser_test
nav2_costmap_2d_core
)
ament_add_gtest(collision_footprint_test footprint_collision_checker_test.cpp)
target_link_libraries(collision_footprint_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(costmap_convesion_test costmap_conversion_test.cpp)
target_link_libraries(costmap_convesion_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(declare_parameter_test declare_parameter_test.cpp)
target_link_libraries(declare_parameter_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(costmap_filter_test costmap_filter_test.cpp)
target_link_libraries(costmap_filter_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(keepout_filter_test keepout_filter_test.cpp)
target_link_libraries(keepout_filter_test
${PROJECT_NAME}::nav2_costmap_2d_core
${PROJECT_NAME}::filters
)
ament_add_gtest(speed_filter_test speed_filter_test.cpp)
target_link_libraries(speed_filter_test
${PROJECT_NAME}::nav2_costmap_2d_core
${PROJECT_NAME}::filters
)
ament_add_gtest(binary_filter_test binary_filter_test.cpp)
target_link_libraries(binary_filter_test
${PROJECT_NAME}::nav2_costmap_2d_core
${PROJECT_NAME}::filters
)
ament_add_gtest(copy_window_test copy_window_test.cpp)
target_link_libraries(copy_window_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(costmap_filter_service_test costmap_filter_service_test.cpp)
target_link_libraries(costmap_filter_service_test
${PROJECT_NAME}::nav2_costmap_2d_core
)
ament_add_gtest(denoise_layer_test denoise_layer_test.cpp image_test.cpp image_processing_test.cpp)
target_link_libraries(denoise_layer_test
${PROJECT_NAME}::nav2_costmap_2d_core
${PROJECT_NAME}::layers
)
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2012, Willow Garage, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Willow Garage, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "nav2_costmap_2d/array_parser.hpp"
TEST(array_parser, basic_operation)
{
std::string error;
std::vector<std::vector<float>> vvf;
vvf = nav2_costmap_2d::parseVVF("[[1, 2.2], [.3, -4e4]]", error);
EXPECT_EQ(2u, vvf.size() );
EXPECT_EQ(2u, vvf[0].size() );
EXPECT_EQ(2u, vvf[1].size() );
EXPECT_EQ(1.0f, vvf[0][0]);
EXPECT_EQ(2.2f, vvf[0][1]);
EXPECT_EQ(0.3f, vvf[1][0]);
EXPECT_EQ(-40000.0f, vvf[1][1]);
EXPECT_EQ("", error);
}
TEST(array_parser, missing_open)
{
std::string error;
std::vector<std::vector<float>> vvf;
vvf = nav2_costmap_2d::parseVVF("[1, 2.2], [.3, -4e4]]", error);
EXPECT_NE(error, "");
}
TEST(array_parser, missing_close)
{
std::string error;
std::vector<std::vector<float>> vvf;
vvf = nav2_costmap_2d::parseVVF("[[1, 2.2], [.3, -4e4]", error);
EXPECT_NE(error, "");
}
TEST(array_parser, wrong_depth)
{
std::string error;
std::vector<std::vector<float>> vvf;
vvf = nav2_costmap_2d::parseVVF("[1, 2.2], [.3, -4e4]", error);
EXPECT_NE(error, "");
}
int main(int argc, char ** argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
@@ -0,0 +1,877 @@
// Copyright (c) 2022 Samsung R&D Institute Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include <chrono>
#include <vector>
#include <tuple>
#include <functional>
#include <stdexcept>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"
#include "tf2_ros/transform_broadcaster.h"
#include "nav2_util/occ_grid_values.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "std_msgs/msg/bool.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
#include "nav2_msgs/msg/costmap_filter_info.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
#include "nav2_costmap_2d/costmap_filters/binary_filter.hpp"
using namespace std::chrono_literals;
static const char FILTER_NAME[]{"binary_filter"};
static const char INFO_TOPIC[]{"costmap_filter_info"};
static const char MASK_TOPIC[]{"mask"};
static const char BINARY_STATE_TOPIC[]{"binary_state"};
static const double NO_TRANSLATION = 0.0;
static const double TRANSLATION_X = 1.0;
static const double TRANSLATION_Y = 1.0;
static const uint8_t INCORRECT_TYPE = 200;
class InfoPublisher : public rclcpp::Node
{
public:
InfoPublisher(uint8_t type, const char * mask_topic, double base, double multiplier)
: Node("costmap_filter_info_pub")
{
publisher_ = this->create_publisher<nav2_msgs::msg::CostmapFilterInfo>(
INFO_TOPIC, rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
std::unique_ptr<nav2_msgs::msg::CostmapFilterInfo> msg =
std::make_unique<nav2_msgs::msg::CostmapFilterInfo>();
msg->type = type;
msg->filter_mask_topic = mask_topic;
msg->base = static_cast<float>(base);
msg->multiplier = static_cast<float>(multiplier);
publisher_->publish(std::move(msg));
}
~InfoPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav2_msgs::msg::CostmapFilterInfo>::SharedPtr publisher_;
}; // InfoPublisher
class MaskPublisher : public rclcpp::Node
{
public:
explicit MaskPublisher(const nav_msgs::msg::OccupancyGrid & mask)
: Node("mask_pub")
{
publisher_ = this->create_publisher<nav_msgs::msg::OccupancyGrid>(
MASK_TOPIC,
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
publisher_->publish(mask);
}
~MaskPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav_msgs::msg::OccupancyGrid>::SharedPtr publisher_;
}; // MaskPublisher
class BinaryStateSubscriber : public rclcpp::Node
{
public:
explicit BinaryStateSubscriber(const std::string & binary_state_topic, bool default_state)
: Node("binary_state_sub"), binary_state_updated_(false)
{
subscriber_ = this->create_subscription<std_msgs::msg::Bool>(
binary_state_topic, rclcpp::QoS(10),
std::bind(&BinaryStateSubscriber::binaryStateCallback, this, std::placeholders::_1));
// Initialize with default state
msg_ = std::make_shared<std_msgs::msg::Bool>();
msg_->data = default_state;
}
void binaryStateCallback(
const std_msgs::msg::Bool::SharedPtr msg)
{
msg_ = msg;
binary_state_updated_ = true;
}
std_msgs::msg::Bool::SharedPtr getBinaryState()
{
return msg_;
}
inline bool binaryStateUpdated()
{
return binary_state_updated_;
}
inline void resetBinaryStateIndicator()
{
binary_state_updated_ = false;
}
private:
rclcpp::Subscription<std_msgs::msg::Bool>::SharedPtr subscriber_;
std_msgs::msg::Bool::SharedPtr msg_;
bool binary_state_updated_;
}; // BinaryStateSubscriber
class TestMask : public nav_msgs::msg::OccupancyGrid
{
public:
TestMask(
unsigned int width, unsigned int height, double resolution,
const std::string & mask_frame)
: width_(width), height_(height)
{
// Fill filter mask info
header.frame_id = mask_frame;
info.resolution = resolution;
info.width = width_;
info.height = height_;
info.origin.position.x = 0.0;
info.origin.position.y = 0.0;
info.origin.position.z = 0.0;
info.origin.orientation.x = 0.0;
info.origin.orientation.y = 0.0;
info.origin.orientation.z = 0.0;
info.origin.orientation.w = 1.0;
// Fill test mask as follows:
//
// mask (10,11)
// *----------------*
// |91|92|...|99|100|
// |... |
// |... |
// |11|12|13|...| 20|
// | 1| 2| 3|...| 10|
// |-1| 0| 0|...| 0|
// *----------------*
// (0,0)
data.resize(width_ * height_, nav2_util::OCC_GRID_UNKNOWN);
unsigned int mx, my;
data[0] = nav2_util::OCC_GRID_UNKNOWN;
for (mx = 1; mx < width_; mx++) {
data[mx] = nav2_util::OCC_GRID_FREE;
}
unsigned int it;
for (my = 1; my < height_; my++) {
for (mx = 0; mx < width_; mx++) {
it = mx + my * width_;
data[it] = makeData(mx, my);
}
}
}
inline int8_t makeData(unsigned int mx, unsigned int my)
{
return mx + (my - 1) * width_ + 1;
}
private:
const unsigned int width_;
const unsigned int height_;
}; // TestMask
class TestNode : public ::testing::Test
{
public:
TestNode()
: default_state_(false) {}
~TestNode() {}
protected:
void createMaps(const std::string & mask_frame);
void publishMaps(uint8_t type, const char * mask_topic, double base, double multiplier);
void rePublishInfo(uint8_t type, const char * mask_topic, double base, double multiplier);
void rePublishMask();
void setDefaultState(bool default_state); // NOTE: must be called before createBinaryFilter()
bool createBinaryFilter(const std::string & global_frame, double flip_threshold);
void createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame);
void publishTransform();
// Test methods
void testFullMask(
double base, double multiplier, double flip_threshold, double tr_x, double tr_y);
void testSimpleMask(
double base, double multiplier, double flip_threshold, double tr_x, double tr_y);
void testOutOfMask();
void testIncorrectTF();
void testResetFilter();
void resetMaps();
void reset();
std::shared_ptr<nav2_costmap_2d::BinaryFilter> binary_filter_;
std::shared_ptr<nav2_costmap_2d::Costmap2D> master_grid_;
bool default_state_;
private:
void waitSome(const std::chrono::nanoseconds & duration);
std_msgs::msg::Bool::SharedPtr getBinaryState();
std_msgs::msg::Bool::SharedPtr waitBinaryState();
bool getSign(
unsigned int x, unsigned int y, double base, double multiplier, double flip_threshold);
void verifyBinaryState(bool sign, std_msgs::msg::Bool::SharedPtr state);
const unsigned int width_ = 10;
const unsigned int height_ = 11;
const double resolution_ = 1.0;
nav2_util::LifecycleNode::SharedPtr node_;
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
std::unique_ptr<geometry_msgs::msg::TransformStamped> transform_;
std::shared_ptr<TestMask> mask_;
std::shared_ptr<InfoPublisher> info_publisher_;
std::shared_ptr<MaskPublisher> mask_publisher_;
std::shared_ptr<BinaryStateSubscriber> binary_state_subscriber_;
};
void TestNode::createMaps(const std::string & mask_frame)
{
// Make map and mask put as follows:
// master_grid (12,13)
// *----------------*
// | |
// | mask (10,11) |
// | *-------* |
// | |///////| |
// | |///////| |
// | |///////| |
// | *-------* |
// | (0,0) |
// | |
// *----------------*
// (-2,-2)
// Create master_grid_
master_grid_ = std::make_shared<nav2_costmap_2d::Costmap2D>(
width_ + 4, height_ + 4, resolution_, -2.0, -2.0, nav2_costmap_2d::FREE_SPACE);
// Create mask_
mask_ = std::make_shared<TestMask>(width_, height_, resolution_, mask_frame);
}
void TestNode::publishMaps(
uint8_t type, const char * mask_topic, double base, double multiplier)
{
info_publisher_ = std::make_shared<InfoPublisher>(type, mask_topic, base, multiplier);
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
}
void TestNode::rePublishInfo(
uint8_t type, const char * mask_topic, double base, double multiplier)
{
info_publisher_.reset();
info_publisher_ = std::make_shared<InfoPublisher>(type, mask_topic, base, multiplier);
// Allow both CostmapFilterInfo and filter mask subscribers
// to receive a new message
waitSome(100ms);
}
void TestNode::rePublishMask()
{
mask_publisher_.reset();
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
// Allow filter mask subscriber to receive a new message
waitSome(100ms);
}
void TestNode::waitSome(const std::chrono::nanoseconds & duration)
{
rclcpp::Time start_time = node_->now();
while (rclcpp::ok() && node_->now() - start_time <= rclcpp::Duration(duration)) {
rclcpp::spin_some(node_->get_node_base_interface());
rclcpp::spin_some(binary_state_subscriber_);
std::this_thread::sleep_for(10ms);
}
}
std_msgs::msg::Bool::SharedPtr TestNode::getBinaryState()
{
std::this_thread::sleep_for(100ms);
rclcpp::spin_some(binary_state_subscriber_);
return binary_state_subscriber_->getBinaryState();
}
std_msgs::msg::Bool::SharedPtr TestNode::waitBinaryState()
{
const std::chrono::nanoseconds timeout = 500ms;
rclcpp::Time start_time = node_->now();
binary_state_subscriber_->resetBinaryStateIndicator();
while (rclcpp::ok() && node_->now() - start_time <= rclcpp::Duration(timeout)) {
if (binary_state_subscriber_->binaryStateUpdated()) {
binary_state_subscriber_->resetBinaryStateIndicator();
return binary_state_subscriber_->getBinaryState();
}
rclcpp::spin_some(binary_state_subscriber_);
std::this_thread::sleep_for(10ms);
}
return nullptr;
}
void TestNode::setDefaultState(bool default_state)
{
default_state_ = default_state;
}
bool TestNode::createBinaryFilter(const std::string & global_frame, double flip_threshold)
{
node_ = std::make_shared<nav2_util::LifecycleNode>("test_node");
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
tf_buffer_->setUsingDedicatedThread(true); // One-thread broadcasting-listening model
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
nav2_costmap_2d::LayeredCostmap layers(global_frame, false, false);
node_->declare_parameter(
std::string(FILTER_NAME) + ".transform_tolerance", rclcpp::ParameterValue(0.5));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".transform_tolerance", 0.5));
node_->declare_parameter(
std::string(FILTER_NAME) + ".filter_info_topic", rclcpp::ParameterValue(INFO_TOPIC));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".filter_info_topic", INFO_TOPIC));
node_->declare_parameter(
std::string(FILTER_NAME) + ".default_state", rclcpp::ParameterValue(default_state_));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".default_state", default_state_));
node_->declare_parameter(
std::string(FILTER_NAME) + ".binary_state_topic", rclcpp::ParameterValue(BINARY_STATE_TOPIC));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".binary_state_topic", BINARY_STATE_TOPIC));
node_->declare_parameter(
std::string(FILTER_NAME) + ".flip_threshold", rclcpp::ParameterValue(flip_threshold));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".flip_threshold", flip_threshold));
binary_filter_ = std::make_shared<nav2_costmap_2d::BinaryFilter>();
binary_filter_->initialize(&layers, FILTER_NAME, tf_buffer_.get(), node_, nullptr);
binary_filter_->initializeFilter(INFO_TOPIC);
binary_state_subscriber_ =
std::make_shared<BinaryStateSubscriber>(BINARY_STATE_TOPIC, default_state_);
// Wait until mask will be received by BinaryFilter
const std::chrono::nanoseconds timeout = 500ms;
rclcpp::Time start_time = node_->now();
while (!binary_filter_->isActive()) {
if (node_->now() - start_time > rclcpp::Duration(timeout)) {
return false;
}
rclcpp::spin_some(node_->get_node_base_interface());
std::this_thread::sleep_for(10ms);
}
return true;
}
void TestNode::createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame)
{
tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(node_);
transform_ = std::make_unique<geometry_msgs::msg::TransformStamped>();
transform_->header.frame_id = mask_frame;
transform_->child_frame_id = global_frame;
transform_->header.stamp = node_->now() + rclcpp::Duration(100ms);
transform_->transform.translation.x = TRANSLATION_X;
transform_->transform.translation.y = TRANSLATION_Y;
transform_->transform.translation.z = 0.0;
transform_->transform.rotation.x = 0.0;
transform_->transform.rotation.y = 0.0;
transform_->transform.rotation.z = 0.0;
transform_->transform.rotation.w = 1.0;
tf_broadcaster_->sendTransform(*transform_);
// Allow tf_buffer_ to be filled by listener
waitSome(100ms);
}
void TestNode::publishTransform()
{
if (tf_broadcaster_) {
transform_->header.stamp = node_->now() + rclcpp::Duration(100ms);
tf_broadcaster_->sendTransform(*transform_);
}
}
bool TestNode::getSign(
unsigned int x, unsigned int y, double base, double multiplier, double flip_threshold)
{
const int8_t cost = mask_->makeData(x, y);
return base + cost * multiplier > flip_threshold;
}
void TestNode::verifyBinaryState(bool sign, std_msgs::msg::Bool::SharedPtr state)
{
ASSERT_TRUE(state != nullptr);
if (sign) {
EXPECT_FALSE(state->data == default_state_);
} else {
EXPECT_TRUE(state->data == default_state_);
}
}
void TestNode::testFullMask(
double base, double multiplier, double flip_threshold, double tr_x, double tr_y)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
std_msgs::msg::Bool::SharedPtr binary_state;
unsigned int x, y;
bool prev_sign = false;
bool sign;
// data = 0
x = 1;
y = 0;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
sign = getSign(x, y, base, multiplier, flip_threshold);
if (sign != prev_sign) {
// Binary filter just flipped
binary_state = waitBinaryState();
prev_sign = sign;
} else {
// Binary filter state should not be changed
binary_state = getBinaryState();
}
verifyBinaryState(sign, binary_state);
// data in range [1..100] (sparsed for testing speed)
for (y = 1; y < height_; y += 2) {
for (x = 0; x < width_; x += 2) {
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
sign = getSign(x, y, base, multiplier, flip_threshold);
if (prev_sign != sign) {
// Binary filter just flipped
binary_state = waitBinaryState();
prev_sign = sign;
} else {
// Binary filter state should not be changed
binary_state = getBinaryState();
}
verifyBinaryState(sign, binary_state);
}
}
// data = -1 (unknown)
bool prev_state = binary_state->data;
pose.x = -tr_x;
pose.y = -tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = getBinaryState();
ASSERT_TRUE(binary_state != nullptr);
ASSERT_EQ(binary_state->data, prev_state); // Binary state won't be updated
}
void TestNode::testSimpleMask(
double base, double multiplier, double flip_threshold, double tr_x, double tr_y)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
std_msgs::msg::Bool::SharedPtr binary_state;
unsigned int x, y;
bool prev_sign = false;
bool sign;
// data = 0
x = 1;
y = 0;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
sign = getSign(x, y, base, multiplier, flip_threshold);
if (sign != prev_sign) {
// Binary filter just flipped
binary_state = waitBinaryState();
prev_sign = sign;
} else {
// Binary filter state should not be changed
binary_state = getBinaryState();
}
verifyBinaryState(sign, binary_state);
// data = <some_middle_value>
x = width_ / 2 - 1;
y = height_ / 2 - 1;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
sign = getSign(x, y, base, multiplier, flip_threshold);
if (prev_sign != sign) {
// Binary filter just flipped
binary_state = waitBinaryState();
prev_sign = sign;
} else {
// Binary filter state should not be changed
binary_state = getBinaryState();
}
verifyBinaryState(sign, binary_state);
// data = 100
x = width_ - 1;
y = height_ - 1;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
sign = getSign(x, y, base, multiplier, flip_threshold);
if (prev_sign != sign) {
// Binary filter just flipped
binary_state = waitBinaryState();
prev_sign = sign;
} else {
// Binary filter state should not be changed
binary_state = getBinaryState();
}
verifyBinaryState(sign, binary_state);
// data = -1 (unknown)
bool prev_state = binary_state->data;
pose.x = -tr_x;
pose.y = -tr_y;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = getBinaryState();
ASSERT_TRUE(binary_state != nullptr);
ASSERT_EQ(binary_state->data, prev_state); // Binary state won't be updated
}
void TestNode::testOutOfMask()
{
// base, multiplier and flip_threshold should have values as below for this test
const double base = 0.0;
const double multiplier = 1.0;
const double flip_threshold = 10.0;
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
std_msgs::msg::Bool::SharedPtr binary_state;
// data = <some_middle_value>
pose.x = width_ / 2 - 1;
pose.y = height_ / 2 - 1;
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = waitBinaryState();
verifyBinaryState(getSign(pose.x, pose.y, base, multiplier, flip_threshold), binary_state);
// Then go to out of mask bounds and ensure that binary state is set back to default
pose.x = -2.0;
pose.y = -2.0;
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = getBinaryState();
ASSERT_TRUE(binary_state != nullptr);
ASSERT_EQ(binary_state->data, default_state_);
pose.x = width_ + 1.0;
pose.y = height_ + 1.0;
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = getBinaryState();
ASSERT_TRUE(binary_state != nullptr);
ASSERT_EQ(binary_state->data, default_state_);
}
void TestNode::testIncorrectTF()
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
std_msgs::msg::Bool::SharedPtr binary_state;
// data = <some_middle_value>
pose.x = width_ / 2 - 1;
pose.y = height_ / 2 - 1;
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = waitBinaryState();
ASSERT_TRUE(binary_state == nullptr);
}
void TestNode::testResetFilter()
{
// base, multiplier and flip_threshold should have values as below for this test
const double base = 0.0;
const double multiplier = 1.0;
const double flip_threshold = 10.0;
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
std_msgs::msg::Bool::SharedPtr binary_state;
// Switch-on binary filter
pose.x = width_ / 2 - 1;
pose.y = height_ / 2 - 1;
publishTransform();
binary_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
binary_state = waitBinaryState();
verifyBinaryState(getSign(pose.x, pose.y, base, multiplier, flip_threshold), binary_state);
// Reset binary filter and check its state was resetted to default
binary_filter_->resetFilter();
binary_state = waitBinaryState();
ASSERT_TRUE(binary_state != nullptr);
ASSERT_EQ(binary_state->data, default_state_);
}
void TestNode::resetMaps()
{
mask_.reset();
master_grid_.reset();
}
void TestNode::reset()
{
resetMaps();
info_publisher_.reset();
mask_publisher_.reset();
binary_state_subscriber_.reset();
binary_filter_.reset();
node_.reset();
tf_listener_.reset();
tf_broadcaster_.reset();
tf_buffer_.reset();
}
TEST_F(TestNode, testBinaryState)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
ASSERT_TRUE(createBinaryFilter("map", 10.0));
// Test BinaryFilter
testSimpleMask(0.0, 1.0, 10.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testBinaryStateScaled)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 100.0, -1.0);
ASSERT_TRUE(createBinaryFilter("map", 35.0));
// Test BinaryFilter
testFullMask(100.0, -1.0, 35.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testInvertedBinaryState)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
setDefaultState(true);
ASSERT_TRUE(createBinaryFilter("map", 10.0));
// Test BinaryFilter
testSimpleMask(0.0, 1.0, 10.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testOutOfBounds)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
ASSERT_TRUE(createBinaryFilter("map", 10.0));
// Test BinaryFilter
testOutOfMask();
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testInfoRePublish)
{
// Initialize test system
createMaps("map");
// Publish Info with incorrect dummy mask topic
publishMaps(nav2_costmap_2d::BINARY_FILTER, "dummy_topic", 0.0, 1.0);
ASSERT_FALSE(createBinaryFilter("map", 10.0));
// Re-publish filter info with correct mask topic
// and ensure that everything works fine
rePublishInfo(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
// Test BinaryFilter
testSimpleMask(0.0, 1.0, 10.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testMaskRePublish)
{
// Create mask in incorrect frame
createMaps("dummy");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
EXPECT_TRUE(createBinaryFilter("map", 10.0));
// Create mask in correct frame
resetMaps();
createMaps("map");
// Re-publish correct filter mask and ensure that everything works fine
rePublishMask();
// Test BinaryFilter
testSimpleMask(0.0, 1.0, 10.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testIncorrectFilterType)
{
// Initialize test system
createMaps("map");
publishMaps(INCORRECT_TYPE, MASK_TOPIC, 0.0, 1.0);
ASSERT_FALSE(createBinaryFilter("map", 10.0));
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testDifferentFrame)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
ASSERT_TRUE(createBinaryFilter("odom", 10.0));
createTFBroadcaster("map", "odom");
// Test BinaryFilter
testSimpleMask(0.0, 1.0, 10.0, TRANSLATION_X, TRANSLATION_Y);
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testIncorrectFrame)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
ASSERT_TRUE(createBinaryFilter("odom", 10.0));
// map->odom TF does not exit
// Test BinaryFilter with incorrect TF chain
testIncorrectTF();
// Clean-up
binary_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testResetState)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::BINARY_FILTER, MASK_TOPIC, 0.0, 1.0);
ASSERT_TRUE(createBinaryFilter("map", 10.0));
testResetFilter();
// Clean-up
// do not need to resetFilter(): this was already done in testResetFilter()
reset();
}
int main(int argc, char ** argv)
{
// Initialize the system
testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
// Actual testing
bool test_result = RUN_ALL_TESTS();
// Shutdown
rclcpp::shutdown();
return test_result;
}
@@ -0,0 +1,52 @@
// Copyright (c) 2021 Samsung Research Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include "rclcpp/rclcpp.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture g_rclcppfixture;
TEST(CopyWindow, copyValidWindow)
{
nav2_costmap_2d::Costmap2D src(10, 10, 0.1, 0.0, 0.0);
nav2_costmap_2d::Costmap2D dst(5, 5, 0.2, 100.0, 100.0);
// Adding 2 marked cells to source costmap
src.setCost(2, 2, 100);
src.setCost(5, 5, 200);
ASSERT_TRUE(dst.copyWindow(src, 2, 2, 6, 6, 0, 0));
// Check that both marked cells were copied to destination costmap
ASSERT_EQ(dst.getCost(0, 0), 100);
ASSERT_EQ(dst.getCost(3, 3), 200);
}
TEST(CopyWindow, copyInvalidWindow)
{
nav2_costmap_2d::Costmap2D src(10, 10, 0.1, 0.0, 0.0);
nav2_costmap_2d::Costmap2D dst(5, 5, 0.2, 100.0, 100.0);
// Case1: incorrect source bounds
ASSERT_FALSE(dst.copyWindow(src, 9, 9, 11, 11, 0, 0));
// Case2: incorrect destination bounds
ASSERT_FALSE(dst.copyWindow(src, 0, 0, 1, 1, 5, 5));
ASSERT_FALSE(dst.copyWindow(src, 0, 0, 6, 6, 0, 0));
}
@@ -0,0 +1,122 @@
// Copyright (c) 2020 Samsung Research Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <vector>
#include <cmath>
#include <limits>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/occ_grid_values.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture g_rclcppfixture;
static constexpr double EPSILON = std::numeric_limits<float>::epsilon();
static constexpr double RESOLUTION = 0.05;
static constexpr double ORIGIN_X = 0.1;
static constexpr double ORIGIN_Y = 0.2;
class TestNode : public ::testing::Test
{
public:
TestNode() {}
~TestNode()
{
occ_grid_.reset();
costmap_.reset();
}
protected:
void createMaps();
void verifyCostmap();
private:
std::shared_ptr<nav_msgs::msg::OccupancyGrid> occ_grid_;
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_;
};
void TestNode::createMaps()
{
// Create occ_grid_ map
occ_grid_ = std::make_shared<nav_msgs::msg::OccupancyGrid>();
const unsigned int width = 4;
const unsigned int height = 3;
occ_grid_->info.resolution = RESOLUTION;
occ_grid_->info.width = width;
occ_grid_->info.height = height;
occ_grid_->info.origin.position.x = ORIGIN_X;
occ_grid_->info.origin.position.y = ORIGIN_Y;
occ_grid_->info.origin.position.z = 0.0;
occ_grid_->info.origin.orientation.x = 0.0;
occ_grid_->info.origin.orientation.y = 0.0;
occ_grid_->info.origin.orientation.z = 0.0;
occ_grid_->info.origin.orientation.w = 1.0;
occ_grid_->data.resize(width * height);
int8_t data;
for (unsigned int i = 0; i < width * height; i++) {
data = i * 10;
if (data <= nav2_util::OCC_GRID_OCCUPIED) {
occ_grid_->data[i] = data;
} else {
occ_grid_->data[i] = nav2_util::OCC_GRID_UNKNOWN;
}
}
// Create costmap_ (convert OccupancyGrid -> to Costmap2D)
costmap_ = std::make_shared<nav2_costmap_2d::Costmap2D>(*occ_grid_);
}
void TestNode::verifyCostmap()
{
// Verify Costmap2D info
EXPECT_NEAR(costmap_->getResolution(), RESOLUTION, EPSILON);
EXPECT_NEAR(costmap_->getOriginX(), ORIGIN_X, EPSILON);
EXPECT_NEAR(costmap_->getOriginY(), ORIGIN_Y, EPSILON);
// Verify Costmap2D data
unsigned int it;
unsigned char data, data_ref;
for (it = 0; it < (costmap_->getSizeInCellsX() * costmap_->getSizeInCellsY() - 1); it++) {
data = costmap_->getCharMap()[it];
if (it != costmap_->getSizeInCellsX() * costmap_->getSizeInCellsY() - 1) {
data_ref = std::round(
static_cast<double>(nav2_costmap_2d::LETHAL_OBSTACLE - nav2_costmap_2d::FREE_SPACE) * it /
10);
} else {
data_ref = nav2_costmap_2d::NO_INFORMATION;
}
EXPECT_EQ(data, data_ref);
}
}
TEST_F(TestNode, convertOccGridToCostmap)
{
createMaps();
verifyCostmap();
}
@@ -0,0 +1,150 @@
// Copyright (c) 2022 Samsung R&D Institute Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "nav2_costmap_2d/costmap_filters/costmap_filter.hpp"
#include "std_srvs/srv/set_bool.hpp"
static const char FILTER_NAME[]{"costmap_filter"};
class CostmapFilterWrapper : public nav2_costmap_2d::CostmapFilter
{
public:
// Dummy implementations of virtual methods
void initializeFilter(
const std::string &) {}
void process(
nav2_costmap_2d::Costmap2D &,
int, int, int, int,
const geometry_msgs::msg::Pose2D &) {}
void resetFilter() {}
// Actual testing methods
void setName(const std::string & name)
{
name_ = name;
}
void setNode(const nav2_util::LifecycleNode::WeakPtr & node)
{
node_ = node;
}
bool getEnabled()
{
return enabled_;
}
};
class TestNode : public ::testing::Test
{
public:
TestNode()
{
// Create new LifecycleNode
node_ = std::make_shared<nav2_util::LifecycleNode>("test_node");
// Create new CostmapFilter
costmap_filter_ = std::make_shared<CostmapFilterWrapper>();
costmap_filter_->setNode(node_);
costmap_filter_->setName(FILTER_NAME);
// Set CostmapFilter ROS-parameters
node_->declare_parameter(
std::string(FILTER_NAME) + ".filter_info_topic", rclcpp::ParameterValue("filter_info"));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".filter_info_topic", "filter_info"));
}
~TestNode()
{
costmap_filter_.reset();
node_.reset();
}
template<class T>
typename T::Response::SharedPtr send_request(
nav2_util::LifecycleNode::SharedPtr node,
typename rclcpp::Client<T>::SharedPtr client,
typename T::Request::SharedPtr request)
{
auto result = client->async_send_request(request);
// Wait for the result
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) {
return result.get();
} else {
return nullptr;
}
}
protected:
nav2_util::LifecycleNode::SharedPtr node_;
std::shared_ptr<CostmapFilterWrapper> costmap_filter_;
};
TEST_F(TestNode, testEnableService)
{
costmap_filter_->onInitialize();
RCLCPP_INFO(node_->get_logger(), "Testing enabling service");
auto req = std::make_shared<std_srvs::srv::SetBool::Request>();
auto client = node_->create_client<std_srvs::srv::SetBool>(
std::string(FILTER_NAME) + "/toggle_filter");
RCLCPP_INFO(node_->get_logger(), "Waiting for enabling service");
ASSERT_TRUE(client->wait_for_service());
// Set costmap filter enabled
req->data = true;
auto resp = send_request<std_srvs::srv::SetBool>(node_, client, req);
ASSERT_NE(resp, nullptr);
ASSERT_TRUE(resp->success);
ASSERT_EQ(resp->message, "Enabled");
ASSERT_TRUE(costmap_filter_->getEnabled());
// Set costmap filter disabled
req->data = false;
resp = send_request<std_srvs::srv::SetBool>(node_, client, req);
ASSERT_NE(resp, nullptr);
ASSERT_TRUE(resp->success);
ASSERT_EQ(resp->message, "Disabled");
ASSERT_FALSE(costmap_filter_->getEnabled());
}
int main(int argc, char ** argv)
{
// Initialize the system
testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
// Actual testing
bool test_result = RUN_ALL_TESTS();
// Shutdown
rclcpp::shutdown();
return test_result;
}
@@ -0,0 +1,105 @@
// Copyright (c) 2023 Samsung R&D Institute Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/occ_grid_values.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
#include "geometry_msgs/msg/pose2_d.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_filters/costmap_filter.hpp"
class CostmapFilterWrapper : public nav2_costmap_2d::CostmapFilter
{
public:
CostmapFilterWrapper() {}
bool worldToMask(
nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask,
double wx, double wy, unsigned int & mx, unsigned int & my) const
{
return nav2_costmap_2d::CostmapFilter::worldToMask(filter_mask, wx, wy, mx, my);
}
// API coverage
void initializeFilter(const std::string &) {}
void process(
nav2_costmap_2d::Costmap2D &, int, int, int, int, const geometry_msgs::msg::Pose2D &)
{}
void resetFilter() {}
};
TEST(CostmapFilter, testWorldToMask)
{
// Create occupancy grid for test as follows:
//
// ^
// | (6,6)
// | *-----*
// | |/////| <- mask
// | |/////|
// | *-----*
// | (3,3)
// *---------------->
// (0,0)
const unsigned int width = 3;
const unsigned int height = 3;
auto mask = std::make_shared<nav_msgs::msg::OccupancyGrid>();
mask->header.frame_id = "map";
mask->info.resolution = 1.0;
mask->info.width = width;
mask->info.height = height;
mask->info.origin.position.x = 3.0;
mask->info.origin.position.y = 3.0;
mask->data.resize(width * height, nav2_util::OCC_GRID_OCCUPIED);
CostmapFilterWrapper cf;
unsigned int mx, my;
// Point inside mask
ASSERT_TRUE(cf.worldToMask(mask, 4.0, 5.0, mx, my));
ASSERT_EQ(mx, 1u);
ASSERT_EQ(my, 2u);
// Corner cases
ASSERT_TRUE(cf.worldToMask(mask, 3.0, 3.0, mx, my));
ASSERT_EQ(mx, 0u);
ASSERT_EQ(my, 0u);
ASSERT_TRUE(cf.worldToMask(mask, 5.9, 5.9, mx, my));
ASSERT_EQ(mx, 2u);
ASSERT_EQ(my, 2u);
// Point outside mask
ASSERT_FALSE(cf.worldToMask(mask, 2.9, 2.9, mx, my));
ASSERT_FALSE(cf.worldToMask(mask, 6.0, 6.0, mx, my));
}
int main(int argc, char ** argv)
{
// Initialize the system
testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
// Actual testing
bool test_result = RUN_ALL_TESTS();
// Shutdown
rclcpp::shutdown();
return test_result;
}
@@ -0,0 +1,75 @@
// Copyright (c) 2020 Samsung Research Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "nav2_costmap_2d/layer.hpp"
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture g_rclcppfixture;
class LayerWrapper : public nav2_costmap_2d::Layer
{
void reset() {}
void updateBounds(double, double, double, double *, double *, double *, double *) {}
void updateCosts(nav2_costmap_2d::Costmap2D &, int, int, int, int) {}
bool isClearable() {return false;}
};
TEST(DeclareParameter, useValidParameter)
{
LayerWrapper layer;
nav2_util::LifecycleNode::SharedPtr node =
std::make_shared<nav2_util::LifecycleNode>("test_node");
tf2_ros::Buffer tf(node->get_clock());
nav2_costmap_2d::LayeredCostmap layers("frame", false, false);
layer.initialize(&layers, "test_layer", &tf, node, nullptr);
layer.declareParameter("test1", rclcpp::ParameterValue("test_val1"));
try {
std::string val = node->get_parameter("test_layer.test1").as_string();
EXPECT_EQ(val, "test_val1");
} catch (rclcpp::exceptions::ParameterNotDeclaredException & ex) {
FAIL() << "test_layer.test1 parameter is not set";
}
}
TEST(DeclareParameter, useInvalidParameter)
{
LayerWrapper layer;
nav2_util::LifecycleNode::SharedPtr node =
std::make_shared<nav2_util::LifecycleNode>("test_node");
tf2_ros::Buffer tf(node->get_clock());
nav2_costmap_2d::LayeredCostmap layers("frame", false, false);
layer.initialize(&layers, "test_layer", &tf, node, nullptr);
layer.declareParameter("test2", rclcpp::PARAMETER_STRING);
try {
std::string val = node->get_parameter("test_layer.test2").as_string();
FAIL() << "Incorrectly handling test_layer.test2 parameter which was not set";
} catch (rclcpp::exceptions::ParameterUninitializedException & ex) {
SUCCEED();
}
}
@@ -0,0 +1,520 @@
// Copyright (c) 2023 Andrey Ryzhikov
//
// 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 <gtest/gtest.h>
#include <string>
#include <vector>
#include <tuple>
#include <stdexcept>
#include <algorithm>
#include "nav2_costmap_2d/denoise_layer.hpp"
#include "image_tests_helper.hpp"
namespace nav2_costmap_2d
{
/**
* @brief nav2_costmap_2d::DenoiseLayer class wrapper
*
* Provides access to DenoiseLayer private methods for testing them in isolation
*/
class DenoiseLayerTester : public ::testing::Test
{
public:
void removeSinglePixels(
Image<uint8_t> & image, ConnectivityType connectivity,
bool no_information_is_obstacle = true)
{
denoise_.group_connectivity_type_ = connectivity;
denoise_.no_information_is_obstacle_ = no_information_is_obstacle;
denoise_.removeSinglePixels(image);
}
void removeGroups(
Image<uint8_t> & image, ConnectivityType connectivity,
size_t minimal_group_size, bool no_information_is_obstacle = true)
{
denoise_.group_connectivity_type_ = connectivity;
denoise_.minimal_group_size_ = minimal_group_size;
denoise_.no_information_is_obstacle_ = no_information_is_obstacle;
denoise_.removeGroups(image);
}
void denoise(
Image<uint8_t> & image, ConnectivityType connectivity,
size_t minimal_group_size, bool no_information_is_obstacle = true)
{
denoise_.group_connectivity_type_ = connectivity;
denoise_.minimal_group_size_ = minimal_group_size;
denoise_.no_information_is_obstacle_ = no_information_is_obstacle;
denoise_.denoise(image);
}
bool reset()
{
denoise_.current_ = true;
denoise_.reset();
return denoise_.current_;
}
static void initialize(nav2_costmap_2d::DenoiseLayer & d)
{
d.onInitialize();
}
static bool & touchCurrent(nav2_costmap_2d::DenoiseLayer & d)
{
return d.current_;
}
static void configure(
nav2_costmap_2d::DenoiseLayer & d, ConnectivityType connectivity, size_t minimal_group_size)
{
d.enabled_ = true;
d.group_connectivity_type_ = connectivity;
d.minimal_group_size_ = minimal_group_size;
}
static std::tuple<bool, ConnectivityType, size_t> getParameters(
const nav2_costmap_2d::DenoiseLayer & d)
{
return std::make_tuple(d.enabled_, d.group_connectivity_type_, d.minimal_group_size_);
}
protected:
std::vector<uint8_t> image_buffer_bytes;
std::vector<uint8_t> image_buffer_bytes2;
std::vector<uint8_t> image_buffer_bytes3;
private:
nav2_costmap_2d::DenoiseLayer denoise_;
};
}
using namespace nav2_costmap_2d;
TEST_F(DenoiseLayerTester, removeSinglePixels4way) {
const auto in = imageFromString<uint8_t>(
"x.x."
"x..x"
".x.."
"xx.x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
"x..."
"x..."
".x.."
"xx..", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way4);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removeSinglePixels4wayNoInformationIsEmpty) {
const std::map<char, uint8_t> legend = {{'.', 0}, {'n', NO_INFORMATION}, {'x', LETHAL_OBSTACLE}};
const auto in = imageFromString<uint8_t>(
"x.x."
"xnnx"
"nxnn"
"xx.x", image_buffer_bytes, legend);
const auto exp = imageFromString<uint8_t>(
"x..."
"xnn."
"nxnn"
"xx..", image_buffer_bytes2, legend);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way4, false);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removeSinglePixels8way) {
const auto in = imageFromString<uint8_t>(
"x.x."
"x..x"
".x.."
"xx.x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
"x.x."
"x..x"
".x.."
"xx..", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way8);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removeSinglePixelsFromExtremelySmallImage) {
{
const auto in = imageFromString<uint8_t>(
"x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way8);
ASSERT_TRUE(isEqual(out, exp));
}
{
const auto in = imageFromString<uint8_t>(
"x."
".x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
"x."
".x", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way8);
ASSERT_TRUE(isEqual(out, exp));
}
{
const auto in = imageFromString<uint8_t>(
"x."
".x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".."
"..", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way4);
ASSERT_TRUE(isEqual(out, exp));
}
}
TEST_F(DenoiseLayerTester, removeSinglePixelsFromNonBinary) {
// buffer for 9 pixels with neutral (between FREE_SPACE and INSCRIBED_INFLATED_OBSTACLE) value
image_buffer_bytes.assign(9, 250);
Image<uint8_t> in = makeImage<uint8_t>(3, 3, image_buffer_bytes);
in.row(1)[1] = NO_INFORMATION;
Image<uint8_t> exp = clone(in, image_buffer_bytes2);
exp.row(1)[1] = FREE_SPACE;
auto out = clone(in, image_buffer_bytes3);
removeSinglePixels(out, ConnectivityType::Way4);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removePixelsGroup4way) {
const auto in = imageFromString<uint8_t>(
".xx..xx"
"..x.x.."
"x..x..x"
"x......"
"...x.xx"
"xxx..xx"
"....xx.", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".xx...."
"..x...."
"......."
"......."
".....xx"
"xxx..xx"
"....xx.", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way4, 3);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removePixelsGroup4wayNoInformationIsEmpty) {
const std::map<char, uint8_t> legend = {{'.', 0}, {'n', NO_INFORMATION}, {'x', LETHAL_OBSTACLE}};
const auto in = imageFromString<uint8_t>(
".xxnnxx"
"..xnx.."
"x..x..x"
"x......"
"nnnxnxx"
"xxx..xx"
"....xx.", image_buffer_bytes, legend);
const auto exp = imageFromString<uint8_t>(
".xxnn.."
"..xn..."
"......."
"......."
"nnn.nxx"
"xxx..xx"
"....xx.", image_buffer_bytes2, legend);
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way4, 3, false);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removePixelsGroup8way) {
const auto in = imageFromString<uint8_t>(
".xx..xx"
"..x.x.."
"x..x..x"
"x......"
"...x.xx"
"xxx..xx"
"....xx.", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".xx..xx"
"..x.x.."
"...x..."
"......."
"...x.xx"
"xxx..xx"
"....xx.", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way8, 3);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, removePixelsGroupFromExtremelySmallImage) {
{
const auto in = imageFromString<uint8_t>(
"x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way8, 3);
ASSERT_TRUE(isEqual(out, exp));
}
{
const auto in = imageFromString<uint8_t>(
"x."
".x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
".."
"..", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way8, 3);
ASSERT_TRUE(isEqual(out, exp));
}
}
TEST_F(DenoiseLayerTester, removePixelsGroupFromNonBinary) {
// buffer for 9 pixels with neutral (between FREE_SPACE and INSCRIBED_INFLATED_OBSTACLE) value
image_buffer_bytes.assign(9, 250);
Image<uint8_t> in = makeImage<uint8_t>(3, 3, image_buffer_bytes);
in.row(1)[1] = 255;
Image<uint8_t> exp = clone(in, image_buffer_bytes2);
exp.row(1)[1] = 0;
auto out = clone(in, image_buffer_bytes3);
removeGroups(out, ConnectivityType::Way4, 2);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, denoiseSingles) {
const auto in = imageFromString<uint8_t>(
"xx."
"..."
"..x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
"xx."
"..."
"...", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
denoise(out, ConnectivityType::Way4, 2);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, denoiseGroups) {
const auto in = imageFromString<uint8_t>(
"xx."
"x.x"
"..x", image_buffer_bytes);
const auto exp = imageFromString<uint8_t>(
"xx."
"x.."
"...", image_buffer_bytes2);
auto out = clone(in, image_buffer_bytes3);
denoise(out, ConnectivityType::Way4, 3);
ASSERT_TRUE(isEqual(out, exp)) <<
"input:" << std::endl << in << std::endl <<
"output:" << std::endl << out << std::endl <<
"expected:" << std::endl << exp;
}
TEST_F(DenoiseLayerTester, denoiseEmpty) {
Image<uint8_t> in;
ASSERT_NO_THROW(denoise(in, ConnectivityType::Way4, 2));
}
TEST_F(DenoiseLayerTester, denoiseNothing) {
Image<uint8_t> in = makeImage<uint8_t>(1, 1, image_buffer_bytes);
ASSERT_NO_THROW(denoise(in, ConnectivityType::Way4, 1));
}
TEST_F(DenoiseLayerTester, constructorAndDestructor) {
ASSERT_NO_THROW(
// []()
{
nav2_costmap_2d::DenoiseLayer layer;
});
}
TEST_F(DenoiseLayerTester, reset) {
ASSERT_FALSE(reset());
}
TEST_F(DenoiseLayerTester, isClearable) {
nav2_costmap_2d::DenoiseLayer layer;
ASSERT_FALSE(layer.isClearable());
}
TEST_F(DenoiseLayerTester, updateBounds) {
nav2_costmap_2d::DenoiseLayer layer;
const std::array<double, 4> region = {1., 2., 3., 4.};
auto r = region;
ASSERT_NO_THROW(layer.updateBounds(0., 0., 0., &r[0], &r[1], &r[2], &r[3]));
ASSERT_EQ(r, region);
}
TEST_F(DenoiseLayerTester, updateCostsIfDisabled) {
nav2_costmap_2d::DenoiseLayer layer;
nav2_costmap_2d::Costmap2D costmap(1, 1, 1., 0., 0., 255);
layer.updateCosts(costmap, 0, 0, 1, 1);
ASSERT_EQ(costmap.getCost(0), 255);
}
TEST_F(DenoiseLayerTester, updateCosts) {
nav2_costmap_2d::DenoiseLayer layer;
nav2_costmap_2d::Costmap2D costmap(1, 1, 1., 0., 0.);
costmap.setCost(0, 0, NO_INFORMATION);
DenoiseLayerTester::configure(layer, ConnectivityType::Way4, 2);
layer.updateCosts(costmap, 0, 0, 1, 1);
ASSERT_EQ(costmap.getCost(0), FREE_SPACE);
}
// Copy paste from declare_parameter_test.cpp
class RclCppFixture
{
public:
RclCppFixture() {rclcpp::init(0, nullptr);}
~RclCppFixture() {rclcpp::shutdown();}
};
RclCppFixture rcl_cpp_fixture;
std::shared_ptr<nav2_costmap_2d::DenoiseLayer> constructLayer(
std::shared_ptr<nav2_util::LifecycleNode> node =
std::make_shared<nav2_util::LifecycleNode>("test_node"))
{
auto tf = std::make_shared<tf2_ros::Buffer>(node->get_clock());
auto layers = std::make_shared<nav2_costmap_2d::LayeredCostmap>("frame", false, false);
auto deleter = [node, tf, layers](nav2_costmap_2d::DenoiseLayer * ptr)
{
delete ptr;
};
auto layer = std::shared_ptr<nav2_costmap_2d::DenoiseLayer>(
new nav2_costmap_2d::DenoiseLayer, deleter);
layer->initialize(layers.get(), "test_layer", tf.get(), node, nullptr);
return layer;
}
TEST_F(DenoiseLayerTester, initializeDefault) {
auto layer = constructLayer();
DenoiseLayerTester::initialize(*layer);
ASSERT_EQ(
DenoiseLayerTester::getParameters(*layer),
std::make_tuple(true, ConnectivityType::Way8, 2));
}
TEST_F(DenoiseLayerTester, initializeCustom) {
auto node = std::make_shared<nav2_util::LifecycleNode>("test_node");
auto layer = constructLayer(node);
node->set_parameter(
rclcpp::Parameter(layer->getFullName("minimal_group_size"), rclcpp::ParameterValue(5)));
node->set_parameter(
rclcpp::Parameter(layer->getFullName("group_connectivity_type"), rclcpp::ParameterValue(4)));
DenoiseLayerTester::initialize(*layer);
ASSERT_EQ(
DenoiseLayerTester::getParameters(*layer),
std::make_tuple(true, ConnectivityType::Way4, 5));
}
TEST_F(DenoiseLayerTester, initializeInvalid) {
auto node = std::make_shared<nav2_util::LifecycleNode>("test_node");
auto layer = constructLayer(node);
node->set_parameter(
rclcpp::Parameter(layer->getFullName("minimal_group_size"), rclcpp::ParameterValue(-1)));
node->set_parameter(
rclcpp::Parameter(layer->getFullName("group_connectivity_type"), rclcpp::ParameterValue(3)));
DenoiseLayerTester::initialize(*layer);
ASSERT_EQ(
DenoiseLayerTester::getParameters(*layer),
std::make_tuple(true, ConnectivityType::Way8, 1));
}
@@ -0,0 +1,252 @@
// Copyright (c) 2020 Shivang Patel
//
// 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 <string>
#include <vector>
#include <memory>
#include "gtest/gtest.h"
#include "nav2_costmap_2d/footprint_collision_checker.hpp"
#include "nav2_costmap_2d/footprint.hpp"
TEST(collision_footprint, test_basic)
{
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_ =
std::make_shared<nav2_costmap_2d::Costmap2D>(100, 100, 0.1, 0, 0, 0);
geometry_msgs::msg::Point p1;
p1.x = -0.5;
p1.y = 0.0;
geometry_msgs::msg::Point p2;
p2.x = 0.0;
p2.y = 0.5;
geometry_msgs::msg::Point p3;
p3.x = 0.5;
p3.y = 0.0;
geometry_msgs::msg::Point p4;
p4.x = 0.0;
p4.y = -0.5;
nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4};
nav2_costmap_2d::FootprintCollisionChecker<std::shared_ptr<nav2_costmap_2d::Costmap2D>>
collision_checker(costmap_);
auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint);
EXPECT_NEAR(value, 0.0, 0.001);
}
TEST(collision_footprint, test_point_cost)
{
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_ =
std::make_shared<nav2_costmap_2d::Costmap2D>(100, 100, 0.1, 0, 0, 0);
nav2_costmap_2d::FootprintCollisionChecker<std::shared_ptr<nav2_costmap_2d::Costmap2D>>
collision_checker(costmap_);
auto value = collision_checker.pointCost(50, 50);
EXPECT_NEAR(value, 0.0, 0.001);
}
TEST(collision_footprint, test_world_to_map)
{
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_ =
std::make_shared<nav2_costmap_2d::Costmap2D>(100, 100, 0.1, 0, 0, 0);
nav2_costmap_2d::FootprintCollisionChecker<std::shared_ptr<nav2_costmap_2d::Costmap2D>>
collision_checker(costmap_);
unsigned int x, y;
collision_checker.worldToMap(1.0, 1.0, x, y);
auto value = collision_checker.pointCost(x, y);
EXPECT_NEAR(value, 0.0, 0.001);
costmap_->setCost(50, 50, 200);
collision_checker.worldToMap(5.0, 5.0, x, y);
EXPECT_NEAR(collision_checker.pointCost(x, y), 200.0, 0.001);
}
TEST(collision_footprint, test_footprint_at_pose_with_movement)
{
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_ =
std::make_shared<nav2_costmap_2d::Costmap2D>(100, 100, 0.1, 0, 0, 254);
for (unsigned int i = 40; i <= 60; ++i) {
for (unsigned int j = 40; j <= 60; ++j) {
costmap_->setCost(i, j, 0);
}
}
geometry_msgs::msg::Point p1;
p1.x = -1.0;
p1.y = 1.0;
geometry_msgs::msg::Point p2;
p2.x = 1.0;
p2.y = 1.0;
geometry_msgs::msg::Point p3;
p3.x = 1.0;
p3.y = -1.0;
geometry_msgs::msg::Point p4;
p4.x = -1.0;
p4.y = -1.0;
nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4};
nav2_costmap_2d::FootprintCollisionChecker<std::shared_ptr<nav2_costmap_2d::Costmap2D>>
collision_checker(costmap_);
auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint);
EXPECT_NEAR(value, 0.0, 0.001);
auto up_value = collision_checker.footprintCostAtPose(5.0, 4.9, 0.0, footprint);
EXPECT_NEAR(up_value, 254.0, 0.001);
auto down_value = collision_checker.footprintCostAtPose(5.0, 5.2, 0.0, footprint);
EXPECT_NEAR(down_value, 254.0, 0.001);
}
TEST(collision_footprint, test_point_and_line_cost)
{
std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap_ =
std::make_shared<nav2_costmap_2d::Costmap2D>(100, 100, 0.10000, 0, 0.0, 0.0);
costmap_->setCost(62, 50, 254);
costmap_->setCost(39, 60, 254);
geometry_msgs::msg::Point p1;
p1.x = -1.0;
p1.y = 1.0;
geometry_msgs::msg::Point p2;
p2.x = 1.0;
p2.y = 1.0;
geometry_msgs::msg::Point p3;
p3.x = 1.0;
p3.y = -1.0;
geometry_msgs::msg::Point p4;
p4.x = -1.0;
p4.y = -1.0;
nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4};
nav2_costmap_2d::FootprintCollisionChecker<std::shared_ptr<nav2_costmap_2d::Costmap2D>>
collision_checker(costmap_);
auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint);
EXPECT_NEAR(value, 0.0, 0.001);
auto left_value = collision_checker.footprintCostAtPose(4.9, 5.0, 0.0, footprint);
EXPECT_NEAR(left_value, 254.0, 0.001);
auto right_value = collision_checker.footprintCostAtPose(5.2, 5.0, 0.0, footprint);
EXPECT_NEAR(right_value, 254.0, 0.001);
}
TEST(collision_footprint, not_enough_points)
{
geometry_msgs::msg::Point p1;
p1.x = 2.0;
p1.y = 2.0;
geometry_msgs::msg::Point p2;
p2.x = -2.0;
p2.y = -2.0;
std::vector<geometry_msgs::msg::Point> footprint = {p1, p2};
double min_dist = 0.0;
double max_dist = 0.0;
nav2_costmap_2d::calculateMinAndMaxDistances(footprint, min_dist, max_dist);
EXPECT_EQ(min_dist, std::numeric_limits<double>::max());
EXPECT_EQ(max_dist, 0.0f);
}
TEST(collision_footprint, to_point_32) {
geometry_msgs::msg::Point p;
p.x = 123.0;
p.y = 456.0;
p.z = 789.0;
geometry_msgs::msg::Point32 p32;
p32 = nav2_costmap_2d::toPoint32(p);
EXPECT_NEAR(p.x, p32.x, 1e-5);
EXPECT_NEAR(p.y, p32.y, 1e-5);
EXPECT_NEAR(p.z, p32.z, 1e-5);
}
TEST(collision_footprint, to_polygon) {
geometry_msgs::msg::Point p1;
p1.x = 1.2;
p1.y = 3.4;
p1.z = 5.1;
geometry_msgs::msg::Point p2;
p2.x = -5.6;
p2.y = -7.8;
p2.z = -9.1;
std::vector<geometry_msgs::msg::Point> pts = {p1, p2};
geometry_msgs::msg::Polygon poly;
poly = nav2_costmap_2d::toPolygon(pts);
EXPECT_EQ(2u, sizeof(poly.points) / sizeof(poly.points[0]));
EXPECT_NEAR(poly.points[0].x, p1.x, 1e-5);
EXPECT_NEAR(poly.points[0].y, p1.y, 1e-5);
EXPECT_NEAR(poly.points[0].z, p1.z, 1e-5);
EXPECT_NEAR(poly.points[1].x, p2.x, 1e-5);
EXPECT_NEAR(poly.points[1].y, p2.y, 1e-5);
EXPECT_NEAR(poly.points[1].z, p2.z, 1e-5);
}
TEST(collision_footprint, make_footprint_from_string) {
std::vector<geometry_msgs::msg::Point> footprint;
bool result = nav2_costmap_2d::makeFootprintFromString(
"[[1, 2.2], [.3, -4e4], [-.3, -4e4], [-1, 2.2]]", footprint);
EXPECT_EQ(result, true);
EXPECT_EQ(4u, footprint.size());
EXPECT_NEAR(footprint[0].x, 1.0, 1e-5);
EXPECT_NEAR(footprint[0].y, 2.2, 1e-5);
EXPECT_NEAR(footprint[1].x, 0.3, 1e-5);
EXPECT_NEAR(footprint[1].y, -4e4, 1e-5);
EXPECT_NEAR(footprint[2].x, -0.3, 1e-5);
EXPECT_NEAR(footprint[2].y, -4e4, 1e-5);
EXPECT_NEAR(footprint[3].x, -1.0, 1e-5);
EXPECT_NEAR(footprint[3].y, 2.2, 1e-5);
}
TEST(collision_footprint, make_footprint_from_string_parse_error) {
std::vector<geometry_msgs::msg::Point> footprint;
bool result = nav2_costmap_2d::makeFootprintFromString(
"[[bad_string", footprint);
EXPECT_EQ(result, false);
}
TEST(collision_footprint, make_footprint_from_string_two_points_error) {
std::vector<geometry_msgs::msg::Point> footprint;
bool result = nav2_costmap_2d::makeFootprintFromString(
"[[1, 2.2], [.3, -4e4]", footprint);
EXPECT_EQ(result, false);
}
TEST(collision_footprint, make_footprint_from_string_not_pairs) {
std::vector<geometry_msgs::msg::Point> footprint;
bool result = nav2_costmap_2d::makeFootprintFromString(
"[[1, 2.2], [.3, -4e4], [-.3, -4e4], [-1, 2.2, 5.6]]", footprint);
EXPECT_EQ(result, false);
}
@@ -0,0 +1,527 @@
// Copyright (c) 2023 Andrey Ryzhikov
//
// 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 <gtest/gtest.h>
#include <cmath>
#include "nav2_costmap_2d/denoise/image_processing.hpp"
#include "image_tests_helper.hpp"
using namespace nav2_costmap_2d;
using namespace imgproc_impl;
struct ImageProcTester : public ::testing::Test
{
protected:
std::vector<uint8_t> image_buffer_bytes_;
std::vector<uint8_t> image_buffer_bytes2_;
std::vector<uint8_t> image_buffer_bytes3_;
std::vector<uint16_t> image_buffer_words_;
};
TEST(OutOfBounds, outOfBoundsAccess) {
// check access to nullptr row (up)
{
out_of_bounds_policy::ReplaceToZero<uint8_t> c(nullptr, nullptr, 2);
uint8_t * any_non_null = reinterpret_cast<uint8_t *>(&c);
ASSERT_EQ(c.up(any_non_null), uint8_t(0));
}
// check out of bounds access
{
std::array<uint8_t, 3> data = {1, 2, 3};
out_of_bounds_policy::ReplaceToZero<uint8_t> c(data.data(), data.data(), 2);
auto left_out_of_bounds = std::prev(data.data());
auto right_out_of_bounds = data.data() + data.size();
ASSERT_EQ(c.up(left_out_of_bounds), uint8_t(0));
ASSERT_EQ(c.up(right_out_of_bounds), uint8_t(0));
ASSERT_EQ(c.down(left_out_of_bounds), uint8_t(0));
ASSERT_EQ(c.down(right_out_of_bounds), uint8_t(0));
}
}
TEST_F(ImageProcTester, calculateHistogramWithoutTruncation) {
image_buffer_words_ = {0, 2, 1, 0, 3, 4, 1, 2, 0};
Image<uint16_t> image = makeImage(3, 3, image_buffer_words_);
const uint16_t max_bin_size = 3; // three zeros
const uint16_t max_value = 4;
const auto hist = histogram(image, max_value, max_bin_size);
const std::array<uint8_t, 5> expected = {3, 2, 2, 1, 1};
ASSERT_EQ(hist.size(), expected.size());
ASSERT_TRUE(std::equal(expected.begin(), expected.end(), hist.begin()));
}
TEST_F(ImageProcTester, calculateHistogramWithTruncation) {
image_buffer_words_ = {0, 2, 1, 0, 3, 4, 1, 2, 0};
Image<uint16_t> image = makeImage(3, 3, image_buffer_words_);
const uint16_t max_bin_size = 2;
const uint16_t max_value = 4;
const auto hist = histogram(image, max_value, max_bin_size);
const std::array<uint8_t, 5> expected = {2, 2, 2, 1, 1};
ASSERT_EQ(hist.size(), expected.size());
ASSERT_TRUE(std::equal(expected.begin(), expected.end(), hist.begin()));
}
TEST_F(ImageProcTester, calculateHistogramOfEmpty) {
const uint16_t max_bin_size = 1;
const uint16_t max_value = 0;
Image<uint16_t> empty;
const auto hist = histogram(empty, max_value, max_bin_size);
ASSERT_TRUE(hist.empty());
}
TEST(EquivalenceLabelTrees, newLabelsTest) {
EquivalenceLabelTrees<uint8_t> eq;
eq.reset(10, 10, ConnectivityType::Way4);
ASSERT_EQ(eq.makeLabel(), 1);
ASSERT_EQ(eq.makeLabel(), 2);
ASSERT_EQ(eq.makeLabel(), 3);
}
TEST(EquivalenceLabelTrees, unionTest) {
EquivalenceLabelTrees<uint8_t> eq;
eq.reset(10, 10, ConnectivityType::Way4);
// create 5 single nodes
for (size_t i = 1; i < 6; ++i) {
eq.makeLabel();
}
ASSERT_EQ(eq.unionTrees(4, 3), 3);
ASSERT_EQ(eq.unionTrees(5, 3), 3);
ASSERT_EQ(eq.unionTrees(4, 1), 1);
ASSERT_EQ(eq.unionTrees(2, 5), 1);
}
struct ConnectedComponentsTester : public ImageProcTester
{
protected:
template<ConnectivityType connectivity>
bool fingerTest();
template<ConnectivityType connectivity>
bool spiralTest();
inline static bool isBackground(uint8_t pixel)
{
return pixel == BACKGROUND_CODE;
}
inline Image<uint8_t> makeChessboardLikeImage(
size_t rows, size_t cols,
std::vector<uint8_t> & buffer) const;
protected:
MemoryBuffer buffer_;
imgproc_impl::EquivalenceLabelTrees<uint8_t> label_trees_;
static const uint8_t BACKGROUND_CODE = 0;
static const uint8_t FOREGROUND_CODE = 255;
};
Image<uint8_t> ConnectedComponentsTester::makeChessboardLikeImage(
size_t rows, size_t cols,
std::vector<uint8_t> & buffer) const
{
Image<uint8_t> image = makeImage<uint8_t>(rows, cols, buffer, cols * 3);
auto inverse = [](uint8_t v) {
return (v == BACKGROUND_CODE) ? FOREGROUND_CODE : BACKGROUND_CODE;
};
uint8_t current_value = FOREGROUND_CODE;
for (size_t j = 0; j < cols; ++j) {
*(image.row(0) + j) = current_value;
current_value = inverse(current_value);
}
for (size_t i = 1; i < rows; ++i) {
auto up = image.row(i - 1);
auto current = image.row(i);
for (size_t j = 0; j < cols; ++j, ++up, ++current) {
*current = inverse(*up);
}
}
return image;
}
TEST_F(ConnectedComponentsTester, way4EmptyTest) {
Image<uint8_t> empty;
uint8_t total_labels;
connectedComponents<ConnectivityType::Way4>(
empty, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(0));
}
TEST_F(ConnectedComponentsTester, way4SinglePixelTest) {
Image<uint8_t> input = makeImage(1, 1, image_buffer_bytes_);
uint8_t total_labels;
{
input.row(0)[0] = BACKGROUND_CODE;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(result.row(0)[0], 0);
ASSERT_EQ(total_labels, 1);
}
{
input.row(0)[0] = FOREGROUND_CODE;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(result.row(0)[0], 1);
ASSERT_EQ(total_labels, 2);
}
}
TEST_F(ConnectedComponentsTester, way4ImageSmallTest) {
{
Image<uint8_t> input = makeImage(1, 2, image_buffer_bytes_);
uint8_t total_labels;
input.row(0)[0] = BACKGROUND_CODE;
input.row(0)[1] = FOREGROUND_CODE;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(2));
ASSERT_EQ(result.row(0)[0], 0);
ASSERT_EQ(result.row(0)[1], 1);
}
{
Image<uint8_t> input = makeImage(2, 1, image_buffer_bytes_);
uint8_t total_labels;
input.row(0)[0] = BACKGROUND_CODE;
input.row(1)[0] = FOREGROUND_CODE;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(2));
ASSERT_EQ(result.row(0)[0], 0);
ASSERT_EQ(result.row(1)[0], 1);
}
}
TEST_F(ConnectedComponentsTester, way4LabelsOverflowTest) {
// big chessboard image
Image<uint8_t> input = makeChessboardLikeImage(32, 17, image_buffer_bytes_);
uint8_t total_labels;
ASSERT_THROW(
(connectedComponents<ConnectivityType::Way4>(input, buffer_, label_trees_,
isBackground, total_labels)),
LabelOverflow);
}
template<class T>
struct UniformLabel
{
const Image<T> & labels;
std::map<T, T> labels_map = {{0, 0}};
size_t next_label = 1;
T at(size_t i, size_t j)
{
const T label = labels.row(i)[j];
if (labels_map.find(label) == labels_map.end()) {
labels_map[label] = next_label++;
}
return labels_map[label];
}
};
template<class T>
bool isEqualLabels(const Image<T> & lhs, const Image<T> & rhs)
{
UniformLabel<T> l = {lhs};
UniformLabel<T> r = {rhs};
for (size_t i = 0; i < lhs.rows(); ++i) {
for (size_t j = 0; j < lhs.rows(); ++j) {
if (l.at(i, j) != r.at(i, j)) {
return false;
}
}
}
return true;
}
TEST_F(ConnectedComponentsTester, way4ImageStepsTest) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"..xx"
".xx."
"xx.."
"....", image_buffer_bytes_);
const Image<uint8_t> expected_labels = imageFromString<uint8_t>(
"..xx"
".xx."
"xx.."
"....", image_buffer_bytes2_);
uint8_t total_labels;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(2));
ASSERT_TRUE(isEqualLabels(result, expected_labels));
}
/// @brief create mapping '.'->0, 'a'->1, 'b'->2, ... max_symbol->n
std::map<char, uint8_t> makeLabelsMap(char max_symbol)
{
std::map<char, uint8_t> labels_map = {{'.', 0}};
for (char s = 'a'; s <= max_symbol; ++s) {
labels_map.emplace(s, uint8_t(s - 'a' + 1));
}
return labels_map;
}
TEST_F(ConnectedComponentsTester, way8ImageStepsTest) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"....xx"
"..xx.."
"xx...."
"...xx."
".....x"
"....x.", image_buffer_bytes_);
const Image<uint8_t> expected_labels = imageFromString<uint8_t>(
"....aa"
"..aa.."
"aa...."
"...bb."
".....b"
"....b.", image_buffer_bytes2_, makeLabelsMap('b'));
uint8_t total_labels;
const auto result = connectedComponents<ConnectivityType::Way8>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(3));
ASSERT_TRUE(isEqualLabels(result, expected_labels));
}
TEST_F(ConnectedComponentsTester, way4ImageSieveTest) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"x.x.x"
".x.x."
"x.x.x"
".x.x."
"x.x.x", image_buffer_bytes_);
const Image<uint8_t> expected_labels = imageFromString<uint8_t>(
"a.b.c"
".d.e."
"f.g.h"
".i.j."
"k.l.m", image_buffer_bytes2_, makeLabelsMap('m'));
uint8_t total_labels;
const auto result = connectedComponents<ConnectivityType::Way4>(
input, buffer_, label_trees_,
isBackground, total_labels);
ASSERT_EQ(total_labels, uint8_t(14));
ASSERT_TRUE(isEqualLabels(result, expected_labels));
}
template<ConnectivityType connectivity>
bool ConnectedComponentsTester::fingerTest()
{
const Image<uint8_t> input = imageFromString<uint8_t>(
"....."
"....x"
"..x.x"
"x.x.x"
"x.x.x", image_buffer_bytes_);
const Image<uint8_t> expected_labels = imageFromString<uint8_t>(
"....."
"....c"
"..b.c"
"a.b.c"
"a.b.c", image_buffer_bytes2_, makeLabelsMap('c'));
uint8_t total_labels;
const auto result = connectedComponents<connectivity>(input, buffer_,
label_trees_, isBackground, total_labels);
return total_labels == 4 && isEqualLabels(result, expected_labels);
}
TEST_F(ConnectedComponentsTester, way4ImageFingerTest) {
ASSERT_TRUE(fingerTest<ConnectivityType::Way4>());
}
TEST_F(ConnectedComponentsTester, way8ImageFingerTest) {
ASSERT_TRUE(fingerTest<ConnectivityType::Way8>());
}
template<ConnectivityType connectivity>
bool ConnectedComponentsTester::spiralTest()
{
const Image<uint8_t> input = imageFromString<uint8_t>(
".xxxxxx"
"......x"
".xxxx.x"
".x..x.x"
".x.xx.x"
".x....x"
".xxxxxx", image_buffer_bytes_);
const Image<uint8_t> expected_labels = imageFromString<uint8_t>(
".xxxxxx"
"......x"
".xxxx.x"
".x..x.x"
".x.xx.x"
".x....x"
".xxxxxx", image_buffer_bytes2_);
uint8_t total_labels;
const auto result = connectedComponents<connectivity>(input, buffer_,
label_trees_, isBackground, total_labels);
return total_labels == 2 && isEqualLabels(result, expected_labels);
}
TEST_F(ConnectedComponentsTester, way4ImageSpiralTest) {
ASSERT_TRUE(spiralTest<ConnectivityType::Way4>());
}
TEST_F(ConnectedComponentsTester, way8ImageSpiralTest) {
ASSERT_TRUE(spiralTest<ConnectivityType::Way8>());
}
TEST_F(ConnectedComponentsTester, groupsRemoverUint16LabelOverflow) {
Image<uint8_t> image = makeChessboardLikeImage(512, 512, image_buffer_bytes_);
GroupsRemover remover;
MemoryBuffer buffer;
remover.removeGroups(image, buffer, ConnectivityType::Way4, 2, isBackground);
const auto bg = BACKGROUND_CODE;
image.forEach([bg](uint8_t v) {ASSERT_EQ(v, bg);});
}
ShapeBuffer3x3 shape_buffer{};
const Image<uint8_t> cross_shape = createShape(shape_buffer, ConnectivityType::Way4);
uint8_t max_list(std::initializer_list<uint8_t> lst)
{
return std::max(lst);
}
TEST_F(ImageProcTester, emptyImage) {
Image<uint8_t> input;
Image<uint8_t> output;
ASSERT_NO_THROW(morphologyOperation(input, output, cross_shape, max_list));
}
TEST_F(ImageProcTester, wrongShapeSize) {
Image<uint8_t> input = makeImage(1, 1, image_buffer_bytes_);
Image<uint8_t> output = makeImage(1, 1, image_buffer_bytes2_);
ASSERT_THROW(
morphologyOperation(input, output, makeImage(2, 2, image_buffer_bytes3_), max_list),
std::logic_error);
}
TEST_F(ImageProcTester, wrongSize) {
Image<uint8_t> input = makeImage(3, 2, image_buffer_bytes_);
{
Image<uint8_t> output = makeImage(2, 2, image_buffer_bytes2_);
ASSERT_THROW(
morphologyOperation(input, output, cross_shape, max_list),
std::logic_error);
}
{
Image<uint8_t> output = makeImage(3, 3, image_buffer_bytes2_);
ASSERT_THROW(
morphologyOperation(input, output, cross_shape, max_list),
std::logic_error);
}
}
TEST_F(ImageProcTester, singlePixelImage) {
image_buffer_bytes_ = {255};
Image<uint8_t> input = makeImage(1, 1, image_buffer_bytes_);
Image<uint8_t> output = makeImage(1, 1, image_buffer_bytes2_);
morphologyOperation(input, output, cross_shape, max_list);
ASSERT_EQ(output.row(0)[0], 0);
}
TEST_F(ImageProcTester, cornersImage) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"x..x"
"...."
"...."
"x..x", image_buffer_bytes_);
Image<uint8_t> expected = imageFromString<uint8_t>(
".xx."
"x..x"
"x..x"
".xx.", image_buffer_bytes2_);
Image<uint8_t> output = makeImage(input.rows(), input.columns(), image_buffer_bytes3_);
morphologyOperation(input, output, cross_shape, max_list);
ASSERT_TRUE(isEqual(output, expected));
}
TEST_F(ImageProcTester, horizontalBordersImage) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"x..x"
"x..x"
"x..x"
"x..x", image_buffer_bytes_);
Image<uint8_t> expected = imageFromString<uint8_t>(
"xxxx"
"xxxx"
"xxxx"
"xxxx", image_buffer_bytes2_);
Image<uint8_t> output = makeImage(input.rows(), input.columns(), image_buffer_bytes3_);
morphologyOperation(input, output, cross_shape, max_list);
ASSERT_TRUE(isEqual(output, expected));
}
TEST_F(ImageProcTester, verticalBordersImage) {
const Image<uint8_t> input = imageFromString<uint8_t>(
"xxxx"
"...."
"...."
"xxxx", image_buffer_bytes_);
Image<uint8_t> expected = imageFromString<uint8_t>(
"xxxx"
"xxxx"
"xxxx"
"xxxx", image_buffer_bytes2_);
Image<uint8_t> output = makeImage(input.rows(), input.columns(), image_buffer_bytes3_);
morphologyOperation(input, output, cross_shape, max_list);
ASSERT_TRUE(isEqual(output, expected));
}
@@ -0,0 +1,110 @@
// Copyright (c) 2023 Andrey Ryzhikov
//
// 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 <gtest/gtest.h>
#include "nav2_costmap_2d/denoise/image.hpp"
#include "image_tests_helper.hpp"
using namespace nav2_costmap_2d;
struct ImageTester : public ::testing::Test
{
protected:
std::vector<uint8_t> image_buffer_bytes;
std::vector<uint16_t> image_buffer_words;
};
TEST_F(ImageTester, emptyProps) {
Image<uint8_t> empty;
ASSERT_EQ(empty.rows(), 0ul);
ASSERT_EQ(empty.columns(), 0ul);
ASSERT_EQ(empty.step(), 0ul);
}
TEST_F(ImageTester, memoryAccess) {
std::array<uint8_t, 7> buffer{};
for (uint8_t i = 0; i < buffer.size(); ++i) {
buffer[i] = i;
}
// buffer[3] is unused
Image<uint8_t> wrapper(2, 3, buffer.data(), 4);
ASSERT_EQ(wrapper.row(0), buffer.data());
ASSERT_EQ(wrapper.row(1), buffer.data() + 4);
}
TEST_F(ImageTester, forEach) {
Image<uint8_t> image = makeImage(3, 2, image_buffer_bytes);
const uint8_t non_zero_initial_value = 42;
uint8_t current(non_zero_initial_value);
image.forEach(
[&](uint8_t & pixel) {
pixel = current++;
});
for (size_t i = 0; i < image.rows(); ++i) {
for (size_t j = 0; j < image.columns(); ++j) {
ASSERT_EQ(image.row(i)[j], non_zero_initial_value + i * image.columns() + j);
}
}
}
TEST_F(ImageTester, convert) {
image_buffer_words = {1, 2, 3, 4, 5, 6};
Image<uint16_t> source = makeImage(2, 3, image_buffer_words);
Image<uint8_t> target = makeImage(2, 3, image_buffer_bytes);
source.convert(
target, [](uint16_t s, uint8_t & t) {
t = s * 2;
});
const std::array<uint8_t, 6> expected = {2, 4, 6, 8, 10, 12};
ASSERT_TRUE(std::equal(expected.begin(), expected.end(), image_buffer_bytes.begin()));
}
TEST_F(ImageTester, convertDifferentSizes) {
Image<uint16_t> source = makeImage(2, 3, image_buffer_words);
Image<uint8_t> target = makeImage(3, 2, image_buffer_bytes);
auto do_nothing = [](uint16_t /*src*/, uint8_t & /*trg*/) {};
// Extra parentheses need to protect commas in template arguments
ASSERT_THROW((source.convert(target, do_nothing)), std::logic_error);
}
TEST_F(ImageTester, convertEmptyImages) {
const Image<uint16_t> source;
Image<uint8_t> target;
auto shouldn_t_be_called = [](uint16_t /*src*/, uint8_t & /*trg*/) {
throw std::logic_error("");
};
ASSERT_NO_THROW((source.convert(target, shouldn_t_be_called)));
}
TEST_F(ImageTester, convertWrongSize) {
Image<uint16_t> source = makeImage(2, 3, image_buffer_words);
auto do_nothing = [](uint16_t /*src*/, uint8_t & /*trg*/) {};
{
Image<uint8_t> target = makeImage(3, 3, image_buffer_bytes);
ASSERT_THROW((source.convert(target, do_nothing)), std::logic_error);
}
{
Image<uint8_t> target = makeImage(2, 2, image_buffer_bytes);
ASSERT_THROW((source.convert(target, do_nothing)), std::logic_error);
}
}
@@ -0,0 +1,124 @@
// Copyright (c) 2023 Andrey Ryzhikov
//
// 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.
#ifndef NAV2_COSTMAP_2D__IMAGE_TESTS_HELPER_HPP_
#define NAV2_COSTMAP_2D__IMAGE_TESTS_HELPER_HPP_
#include "nav2_costmap_2d/denoise/image.hpp"
#include <cmath>
#include <string>
#include <vector>
#include <map>
#include <stdexcept>
#include <algorithm>
namespace nav2_costmap_2d
{
template<class T>
Image<T> makeImage(size_t rows, size_t columns, std::vector<T> & buffer, size_t step = 0)
{
step = std::max(step, columns);
buffer.resize(rows * step);
return Image<T>(rows, columns, buffer.data(), step);
}
template<class T>
Image<T> clone(const Image<T> & source, std::vector<T> & buffer)
{
buffer.resize(source.rows() * source.columns());
Image<T> result(source.rows(), source.columns(), buffer.data(), source.columns());
for (size_t row = 0; row < source.rows(); ++row) {
for (size_t column = 0; column < source.columns(); ++column) {
result.row(row)[column] = source.row(row)[column];
}
}
return result;
}
/**
* @brief Decodes image from a string
*
* Used only for tests.
* Each character of the string will be replaced with a code from the codes
* and written to the corresponding pixel of the image
* The image is always square, i.e. the number of rows is equal to the number of columns
* For example, string
* "x.x"
* ".x."
* "..."
* describes a 3x3 image in which a v-shape is drawn with code 255 (with default codes map)
* @throw std::logic_error if the format of the string is incorrect
*/
template<class T>
Image<T> imageFromString(
const std::string & s, std::vector<T> & buffer,
const std::map<char, T> & codes = {{'.', 0}, {'x', 255}})
{
const size_t side_size = static_cast<size_t>(std::sqrt(s.size()));
if (size_t(side_size) * side_size != s.size()) {
throw std::logic_error("Test data error: parseBinaryMatrix: Unexpected input string size");
}
const size_t step = static_cast<size_t>(side_size * 3);
Image<T> image = makeImage(side_size, side_size, buffer, step);
auto iter = s.begin();
image.forEach(
[&](T & pixel) {
try {
pixel = codes.at(*iter);
++iter;
} catch (...) {
throw std::logic_error(
"Test data error: parseBinaryMatrix: Unexpected symbol: " +
std::string(1, *iter));
}
});
return image;
}
/**
* @brief Checks exact match of images
*
* @return true if images a and b have the same type, size, and data. Otherwise false
*/
inline bool isEqual(const Image<uint8_t> & a, const Image<uint8_t> & b)
{
bool equal = a.rows() == b.rows() && a.columns() == b.columns();
for (size_t row = 0; row < a.rows() && equal; ++row) {
for (size_t column = 0; column < a.columns() && equal; ++column) {
equal = a.row(row)[column] == b.row(row)[column];
}
}
return equal;
}
template<class T>
std::ostream & operator<<(std::ostream & out, const Image<T> & image)
{
for (size_t i = 0; i < image.rows(); ++i) {
for (size_t j = 0; j < image.columns(); ++j) {
out << int64_t(image.row(i)[j]) << " ";
}
out << std::endl;
}
return out;
}
} // namespace nav2_costmap_2d
#endif // NAV2_COSTMAP_2D__IMAGE_TESTS_HELPER_HPP_
@@ -0,0 +1,481 @@
// Copyright (c) 2020 Samsung Research Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include <chrono>
#include <vector>
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"
#include "tf2_ros/transform_broadcaster.h"
#include "nav2_util/occ_grid_values.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
#include "nav2_msgs/msg/costmap_filter_info.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_filters/keepout_filter.hpp"
using namespace std::chrono_literals;
static const char FILTER_NAME[]{"keepout_filter"};
static const char INFO_TOPIC[]{"costmap_filter_info"};
static const char MASK_TOPIC[]{"mask"};
class InfoPublisher : public rclcpp::Node
{
public:
InfoPublisher(double base, double multiplier)
: Node("costmap_filter_info_pub")
{
publisher_ = this->create_publisher<nav2_msgs::msg::CostmapFilterInfo>(
INFO_TOPIC, rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
std::unique_ptr<nav2_msgs::msg::CostmapFilterInfo> msg =
std::make_unique<nav2_msgs::msg::CostmapFilterInfo>();
msg->type = 0;
msg->filter_mask_topic = MASK_TOPIC;
msg->base = static_cast<float>(base);
msg->multiplier = static_cast<float>(multiplier);
publisher_->publish(std::move(msg));
}
~InfoPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav2_msgs::msg::CostmapFilterInfo>::SharedPtr publisher_;
}; // InfoPublisher
class MaskPublisher : public rclcpp::Node
{
public:
explicit MaskPublisher(const nav_msgs::msg::OccupancyGrid & mask)
: Node("mask_pub")
{
publisher_ = this->create_publisher<nav_msgs::msg::OccupancyGrid>(
MASK_TOPIC,
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
publisher_->publish(mask);
}
~MaskPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav_msgs::msg::OccupancyGrid>::SharedPtr publisher_;
}; // MaskPublisher
struct Point
{
unsigned int x, y;
};
class TestNode : public ::testing::Test
{
public:
TestNode() {}
~TestNode() {}
protected:
void createMaps(unsigned char master_value, int8_t mask_value, const std::string & mask_frame);
void publishMaps();
void rePublishInfo(double base, double multiplier);
void rePublishMask();
void waitSome(const std::chrono::nanoseconds & duration);
void createKeepoutFilter(const std::string & global_frame);
void createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame);
void verifyMasterGrid(unsigned char free_value, unsigned char keepout_value);
void testStandardScenario(unsigned char free_value, unsigned char keepout_value);
void testFramesScenario(unsigned char free_value, unsigned char keepout_value);
void reset();
std::shared_ptr<nav2_costmap_2d::KeepoutFilter> keepout_filter_;
std::shared_ptr<nav2_costmap_2d::Costmap2D> master_grid_;
std::vector<Point> keepout_points_;
private:
nav2_util::LifecycleNode::SharedPtr node_;
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
std::unique_ptr<geometry_msgs::msg::TransformStamped> transform_;
std::shared_ptr<nav_msgs::msg::OccupancyGrid> mask_;
std::shared_ptr<InfoPublisher> info_publisher_;
std::shared_ptr<MaskPublisher> mask_publisher_;
};
void TestNode::createMaps(
unsigned char master_value, int8_t mask_value, const std::string & mask_frame)
{
// Make map and mask put as follows:
//
// map (10,10)
// *----------------*
// | mask (6,6) |
// | *-----* |
// | |/////| |
// | |/////| |
// | *-----* |
// | (3,3) |
// *----------------*
// (0,0)
const double resolution = 1.0;
// Create master_grid_
unsigned int width = 10;
unsigned int height = 10;
master_grid_ = std::make_shared<nav2_costmap_2d::Costmap2D>(
width, height, resolution, 0.0, 0.0, master_value);
// Create mask_
width = 3;
height = 3;
mask_ = std::make_shared<nav_msgs::msg::OccupancyGrid>();
mask_->info.resolution = resolution;
mask_->header.frame_id = mask_frame;
mask_->info.width = width;
mask_->info.height = height;
mask_->info.origin.position.x = 3.0;
mask_->info.origin.position.y = 3.0;
mask_->info.origin.position.z = 0.0;
mask_->info.origin.orientation.x = 0.0;
mask_->info.origin.orientation.y = 0.0;
mask_->info.origin.orientation.z = 0.0;
mask_->info.origin.orientation.w = 1.0;
mask_->data.resize(width * height, mask_value);
}
void TestNode::publishMaps()
{
info_publisher_ = std::make_shared<InfoPublisher>(0.0, 1.0);
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
}
void TestNode::rePublishInfo(double base, double multiplier)
{
info_publisher_.reset();
info_publisher_ = std::make_shared<InfoPublisher>(base, multiplier);
// Allow both CostmapFilterInfo and filter mask subscribers
// to receive a new message
waitSome(100ms);
}
void TestNode::rePublishMask()
{
mask_publisher_.reset();
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
// Allow filter mask subscriber to receive a new message
waitSome(100ms);
}
void TestNode::waitSome(const std::chrono::nanoseconds & duration)
{
rclcpp::Time start_time = node_->now();
while (rclcpp::ok() && node_->now() - start_time <= rclcpp::Duration(duration)) {
rclcpp::spin_some(node_->get_node_base_interface());
std::this_thread::sleep_for(10ms);
}
}
void TestNode::createKeepoutFilter(const std::string & global_frame)
{
node_ = std::make_shared<nav2_util::LifecycleNode>("test_node");
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
tf_buffer_->setUsingDedicatedThread(true); // One-thread broadcasting-listening model
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
nav2_costmap_2d::LayeredCostmap layers(global_frame, false, false);
node_->declare_parameter(
std::string(FILTER_NAME) + ".transform_tolerance", rclcpp::ParameterValue(0.5));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".transform_tolerance", 0.5));
node_->declare_parameter(
std::string(FILTER_NAME) + ".filter_info_topic", rclcpp::ParameterValue(INFO_TOPIC));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".filter_info_topic", INFO_TOPIC));
keepout_filter_ = std::make_shared<nav2_costmap_2d::KeepoutFilter>();
keepout_filter_->initialize(&layers, std::string(FILTER_NAME), tf_buffer_.get(), node_, nullptr);
keepout_filter_->initializeFilter(INFO_TOPIC);
// Wait until mask will be received by KeepoutFilter
while (!keepout_filter_->isActive()) {
rclcpp::spin_some(node_->get_node_base_interface());
std::this_thread::sleep_for(10ms);
}
}
void TestNode::createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame)
{
tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(node_);
transform_ = std::make_unique<geometry_msgs::msg::TransformStamped>();
transform_->header.frame_id = mask_frame;
transform_->child_frame_id = global_frame;
transform_->header.stamp = node_->now();
transform_->transform.translation.x = 1.0;
transform_->transform.translation.y = 1.0;
transform_->transform.translation.z = 0.0;
transform_->transform.rotation.x = 0.0;
transform_->transform.rotation.y = 0.0;
transform_->transform.rotation.z = 0.0;
transform_->transform.rotation.w = 1.0;
tf_broadcaster_->sendTransform(*transform_);
// Allow tf_buffer_ to be filled by listener
waitSome(100ms);
}
void TestNode::verifyMasterGrid(unsigned char free_value, unsigned char keepout_value)
{
unsigned int x, y;
bool is_checked;
for (y = 0; y < master_grid_->getSizeInCellsY(); y++) {
for (x = 0; x < master_grid_->getSizeInCellsX(); x++) {
is_checked = false;
for (std::vector<Point>::iterator it = keepout_points_.begin();
it != keepout_points_.end(); it++)
{
if (x == it->x && y == it->y) {
EXPECT_EQ(master_grid_->getCost(x, y), keepout_value);
is_checked = true;
break;
}
}
if (!is_checked) {
EXPECT_EQ(master_grid_->getCost(x, y), free_value);
}
}
}
}
void TestNode::testStandardScenario(unsigned char free_value, unsigned char keepout_value)
{
geometry_msgs::msg::Pose2D pose;
// Intersection window: added 4 points
keepout_filter_->process(*master_grid_, 2, 2, 5, 5, pose);
keepout_points_.push_back(Point{3, 3});
keepout_points_.push_back(Point{3, 4});
keepout_points_.push_back(Point{4, 3});
keepout_points_.push_back(Point{4, 4});
verifyMasterGrid(free_value, keepout_value);
// Two windows outside on the horisontal/vertical edge: no new points added
keepout_filter_->process(*master_grid_, 3, 6, 5, 7, pose);
keepout_filter_->process(*master_grid_, 6, 3, 7, 5, pose);
verifyMasterGrid(free_value, keepout_value);
// Corner window: added 1 point
keepout_filter_->process(*master_grid_, 5, 5, 6, 6, pose);
keepout_points_.push_back(Point{5, 5});
verifyMasterGrid(free_value, keepout_value);
// Outside windows: no new points added
keepout_filter_->process(*master_grid_, 0, 0, 2, 2, pose);
keepout_filter_->process(*master_grid_, 0, 7, 2, 9, pose);
keepout_filter_->process(*master_grid_, 7, 0, 9, 2, pose);
keepout_filter_->process(*master_grid_, 7, 7, 9, 9, pose);
verifyMasterGrid(free_value, keepout_value);
}
void TestNode::testFramesScenario(unsigned char free_value, unsigned char keepout_value)
{
geometry_msgs::msg::Pose2D pose;
// Intersection window: added all 9 points because of map->odom frame shift
keepout_filter_->process(*master_grid_, 2, 2, 5, 5, pose);
keepout_points_.push_back(Point{2, 2});
keepout_points_.push_back(Point{2, 3});
keepout_points_.push_back(Point{2, 4});
keepout_points_.push_back(Point{3, 2});
keepout_points_.push_back(Point{3, 3});
keepout_points_.push_back(Point{3, 4});
keepout_points_.push_back(Point{4, 2});
keepout_points_.push_back(Point{4, 3});
keepout_points_.push_back(Point{4, 4});
verifyMasterGrid(free_value, keepout_value);
}
void TestNode::reset()
{
mask_.reset();
master_grid_.reset();
info_publisher_.reset();
mask_publisher_.reset();
keepout_filter_.reset();
node_.reset();
tf_listener_.reset();
tf_broadcaster_.reset();
tf_buffer_.reset();
keepout_points_.clear();
}
TEST_F(TestNode, testFreeMasterLethalKeepout)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_OCCUPIED, "map");
publishMaps();
createKeepoutFilter("map");
// Test KeepoutFilter
testStandardScenario(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testUnknownMasterNonLethalKeepout)
{
// Initialize test system
createMaps(
nav2_costmap_2d::NO_INFORMATION,
(nav2_util::OCC_GRID_OCCUPIED - nav2_util::OCC_GRID_FREE) / 2,
"map");
publishMaps();
createKeepoutFilter("map");
// Test KeepoutFilter
testStandardScenario(
nav2_costmap_2d::NO_INFORMATION,
(nav2_costmap_2d::LETHAL_OBSTACLE - nav2_costmap_2d::FREE_SPACE) / 2);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testFreeKeepout)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_FREE, "map");
publishMaps();
createKeepoutFilter("map");
// Test KeepoutFilter
geometry_msgs::msg::Pose2D pose;
// Check whole area window
keepout_filter_->process(*master_grid_, 0, 0, 10, 10, pose);
// There should be no one point appeared on master_grid_ after process()
verifyMasterGrid(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testUnknownKeepout)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_UNKNOWN, "map");
publishMaps();
createKeepoutFilter("map");
// Test KeepoutFilter
geometry_msgs::msg::Pose2D pose;
// Check whole area window
keepout_filter_->process(*master_grid_, 0, 0, 10, 10, pose);
// There should be no one point appeared on master_grid_ after process()
verifyMasterGrid(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testInfoRePublish)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_OCCUPIED, "map");
publishMaps();
createKeepoutFilter("map");
// Re-publish filter info (with incorrect base and multiplier)
// and test that everything is working after
rePublishInfo(0.1, 0.2);
// Test KeepoutFilter
testStandardScenario(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testMaskRePublish)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_OCCUPIED, "map");
publishMaps();
createKeepoutFilter("map");
// Re-publish filter mask and test that everything is working after
rePublishMask();
// Test KeepoutFilter
testStandardScenario(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testDifferentFrames)
{
// Initialize test system
createMaps(nav2_costmap_2d::FREE_SPACE, nav2_util::OCC_GRID_OCCUPIED, "map");
publishMaps();
createKeepoutFilter("odom");
createTFBroadcaster("map", "odom");
// Test KeepoutFilter
testFramesScenario(nav2_costmap_2d::FREE_SPACE, nav2_costmap_2d::LETHAL_OBSTACLE);
// Clean-up
keepout_filter_->resetFilter();
reset();
}
int main(int argc, char ** argv)
{
// Initialize the system
testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
// Actual testing
bool test_result = RUN_ALL_TESTS();
// Shutdown
rclcpp::shutdown();
return test_result;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -0,0 +1,54 @@
// 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 <memory>
#include "gtest/gtest.h"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "rclcpp/rclcpp.hpp"
#include "lifecycle_msgs/msg/state.hpp"
TEST(LifecylceTest, CheckInitialTfTimeout) {
rclcpp::init(0, nullptr);
auto costmap = std::make_shared<nav2_costmap_2d::Costmap2DROS>(rclcpp::NodeOptions());
costmap->set_parameter({"initial_transform_timeout", 0.0});
std::thread spin_thread{[costmap]() {rclcpp::spin(costmap->get_node_base_interface());}};
{
const auto state_after_configure = costmap->configure();
ASSERT_EQ(state_after_configure.id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
// Without providing the transform from global to robot base the activation should fail
// and the costmap should transition into the inactive state.
const auto state_after_activate = costmap->activate();
ASSERT_EQ(state_after_activate.id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
}
// Set a dummy transform from global to robot base
geometry_msgs::msg::TransformStamped transform_global_to_robot{};
transform_global_to_robot.header.frame_id = costmap->getGlobalFrameID();
transform_global_to_robot.child_frame_id = costmap->getBaseFrameID();
costmap->getTfBuffer()->setTransform(transform_global_to_robot, "test", true);
// Now the costmap should successful transition into the active state
{
const auto state_after_activate = costmap->activate();
ASSERT_EQ(state_after_activate.id(), lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
}
rclcpp::shutdown();
if (spin_thread.joinable()) {
spin_thread.join();
}
}
@@ -0,0 +1,789 @@
// Copyright (c) 2020 Samsung Research Russia
//
// 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. Reserved.
#include <gtest/gtest.h>
#include <string>
#include <memory>
#include <chrono>
#include <vector>
#include <tuple>
#include <functional>
#include <stdexcept>
#include "rclcpp/rclcpp.hpp"
#include "nav2_util/lifecycle_node.hpp"
#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"
#include "tf2_ros/transform_broadcaster.h"
#include "nav2_util/occ_grid_values.hpp"
#include "nav2_costmap_2d/cost_values.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
#include "nav2_msgs/msg/costmap_filter_info.hpp"
#include "nav2_msgs/msg/speed_limit.hpp"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
#include "nav2_costmap_2d/costmap_filters/speed_filter.hpp"
using namespace std::chrono_literals;
static const char FILTER_NAME[]{"speed_filter"};
static const char INFO_TOPIC[]{"costmap_filter_info"};
static const char MASK_TOPIC[]{"mask"};
static const char SPEED_LIMIT_TOPIC[]{"speed_limit"};
static const double NO_TRANSLATION = 0.0;
static const double TRANSLATION_X = 1.0;
static const double TRANSLATION_Y = 1.0;
static const uint8_t INCORRECT_TYPE = 200;
static constexpr double EPSILON = 1e-5;
class InfoPublisher : public rclcpp::Node
{
public:
InfoPublisher(uint8_t type, double base, double multiplier)
: Node("costmap_filter_info_pub")
{
publisher_ = this->create_publisher<nav2_msgs::msg::CostmapFilterInfo>(
INFO_TOPIC, rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
std::unique_ptr<nav2_msgs::msg::CostmapFilterInfo> msg =
std::make_unique<nav2_msgs::msg::CostmapFilterInfo>();
msg->type = type;
msg->filter_mask_topic = MASK_TOPIC;
msg->base = static_cast<float>(base);
msg->multiplier = static_cast<float>(multiplier);
publisher_->publish(std::move(msg));
}
~InfoPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav2_msgs::msg::CostmapFilterInfo>::SharedPtr publisher_;
}; // InfoPublisher
class MaskPublisher : public rclcpp::Node
{
public:
explicit MaskPublisher(const nav_msgs::msg::OccupancyGrid & mask)
: Node("mask_pub")
{
publisher_ = this->create_publisher<nav_msgs::msg::OccupancyGrid>(
MASK_TOPIC,
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
publisher_->publish(mask);
}
~MaskPublisher()
{
publisher_.reset();
}
private:
rclcpp::Publisher<nav_msgs::msg::OccupancyGrid>::SharedPtr publisher_;
}; // MaskPublisher
class SpeedLimitSubscriber : public rclcpp::Node
{
public:
explicit SpeedLimitSubscriber(const std::string & speed_limit_topic)
: Node("speed_limit_sub"), speed_limit_updated_(false)
{
subscriber_ = this->create_subscription<nav2_msgs::msg::SpeedLimit>(
speed_limit_topic, rclcpp::QoS(10),
std::bind(&SpeedLimitSubscriber::speedLimitCallback, this, std::placeholders::_1));
}
void speedLimitCallback(
const nav2_msgs::msg::SpeedLimit::SharedPtr msg)
{
msg_ = msg;
speed_limit_updated_ = true;
}
nav2_msgs::msg::SpeedLimit::SharedPtr getSpeedLimit()
{
return msg_;
}
inline bool speedLimitUpdated()
{
return speed_limit_updated_;
}
inline void resetSpeedLimitIndicator()
{
speed_limit_updated_ = false;
}
private:
rclcpp::Subscription<nav2_msgs::msg::SpeedLimit>::SharedPtr subscriber_;
nav2_msgs::msg::SpeedLimit::SharedPtr msg_;
bool speed_limit_updated_;
}; // SpeedLimitSubscriber
class TestMask : public nav_msgs::msg::OccupancyGrid
{
public:
TestMask(
unsigned int width, unsigned int height, double resolution,
const std::string & mask_frame)
: width_(width), height_(height)
{
// Fill filter mask info
header.frame_id = mask_frame;
info.resolution = resolution;
info.width = width_;
info.height = height_;
info.origin.position.x = 0.0;
info.origin.position.y = 0.0;
info.origin.position.z = 0.0;
info.origin.orientation.x = 0.0;
info.origin.orientation.y = 0.0;
info.origin.orientation.z = 0.0;
info.origin.orientation.w = 1.0;
// Fill test mask as follows:
//
// mask (10,11)
// *----------------*
// |91|92|...|99|100|
// |... |
// |... |
// |11|12|13|...| 20|
// | 1| 2| 3|...| 10|
// |-1| 0| 0|...| 0|
// *----------------*
// (0,0)
data.resize(width_ * height_, nav2_util::OCC_GRID_UNKNOWN);
unsigned int mx, my;
data[0] = -1;
for (mx = 1; mx < width_; mx++) {
data[mx] = 0;
}
unsigned int it;
for (my = 1; my < height_; my++) {
for (mx = 0; mx < width_; mx++) {
it = mx + my * width_;
data[it] = makeData(mx, my);
}
}
}
inline int8_t makeData(unsigned int mx, unsigned int my)
{
return mx + (my - 1) * width_ + 1;
}
private:
const unsigned int width_;
const unsigned int height_;
}; // TestMask
class TestNode : public ::testing::Test
{
public:
TestNode() {}
~TestNode() {}
protected:
void createMaps(const std::string & mask_frame);
void publishMaps(uint8_t type, double base, double multiplier);
void rePublishInfo(uint8_t type, double base, double multiplier);
void rePublishMask();
bool createSpeedFilter(const std::string & global_frame);
void createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame);
void publishTransform();
// Test methods
void testFullMask(
uint8_t type, double base, double multiplier,
double tr_x, double tr_y);
void testSimpleMask(
uint8_t type, double base, double multiplier,
double tr_x, double tr_y);
void testOutOfMask(uint8_t type, double base, double multiplier);
void testIncorrectLimits(uint8_t type, double base, double multiplier);
void reset();
std::shared_ptr<nav2_costmap_2d::SpeedFilter> speed_filter_;
std::shared_ptr<nav2_costmap_2d::Costmap2D> master_grid_;
private:
void waitSome(const std::chrono::nanoseconds & duration);
void verifySpeedLimit(
uint8_t type, double base, double multiplier,
unsigned int x, unsigned int y,
nav2_msgs::msg::SpeedLimit::SharedPtr speed_limit);
nav2_msgs::msg::SpeedLimit::SharedPtr getSpeedLimit();
nav2_msgs::msg::SpeedLimit::SharedPtr waitSpeedLimit();
const unsigned int width_ = 10;
const unsigned int height_ = 11;
const double resolution_ = 1.0;
nav2_util::LifecycleNode::SharedPtr node_;
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
std::unique_ptr<geometry_msgs::msg::TransformStamped> transform_;
std::shared_ptr<TestMask> mask_;
std::shared_ptr<InfoPublisher> info_publisher_;
std::shared_ptr<MaskPublisher> mask_publisher_;
std::shared_ptr<SpeedLimitSubscriber> speed_limit_subscriber_;
};
void TestNode::createMaps(const std::string & mask_frame)
{
// Make map and mask put as follows:
// master_grid (12,13)
// *----------------*
// | |
// | mask (10,11) |
// | *-------* |
// | |///////| |
// | |///////| |
// | |///////| |
// | *-------* |
// | (0,0) |
// | |
// *----------------*
// (-2,-2)
// Create master_grid_
master_grid_ = std::make_shared<nav2_costmap_2d::Costmap2D>(
width_ + 4, height_ + 4, resolution_, -2.0, -2.0, nav2_costmap_2d::FREE_SPACE);
// Create mask_
mask_ = std::make_shared<TestMask>(width_, height_, resolution_, mask_frame);
}
void TestNode::publishMaps(uint8_t type, double base, double multiplier)
{
info_publisher_ = std::make_shared<InfoPublisher>(type, base, multiplier);
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
}
void TestNode::rePublishInfo(uint8_t type, double base, double multiplier)
{
info_publisher_.reset();
info_publisher_ = std::make_shared<InfoPublisher>(type, base, multiplier);
// Allow both CostmapFilterInfo and filter mask subscribers
// to receive a new message
waitSome(100ms);
}
void TestNode::rePublishMask()
{
mask_publisher_.reset();
mask_publisher_ = std::make_shared<MaskPublisher>(*mask_);
// Allow filter mask subscriber to receive a new message
waitSome(100ms);
}
nav2_msgs::msg::SpeedLimit::SharedPtr TestNode::getSpeedLimit()
{
std::this_thread::sleep_for(100ms);
rclcpp::spin_some(speed_limit_subscriber_);
return speed_limit_subscriber_->getSpeedLimit();
}
nav2_msgs::msg::SpeedLimit::SharedPtr TestNode::waitSpeedLimit()
{
const std::chrono::nanoseconds timeout = 500ms;
rclcpp::Time start_time = node_->now();
speed_limit_subscriber_->resetSpeedLimitIndicator();
while (rclcpp::ok() && node_->now() - start_time <= rclcpp::Duration(timeout)) {
if (speed_limit_subscriber_->speedLimitUpdated()) {
speed_limit_subscriber_->resetSpeedLimitIndicator();
return speed_limit_subscriber_->getSpeedLimit();
}
rclcpp::spin_some(speed_limit_subscriber_);
std::this_thread::sleep_for(10ms);
}
return nullptr;
}
void TestNode::waitSome(const std::chrono::nanoseconds & duration)
{
rclcpp::Time start_time = node_->now();
while (rclcpp::ok() && node_->now() - start_time <= rclcpp::Duration(duration)) {
rclcpp::spin_some(node_->get_node_base_interface());
rclcpp::spin_some(speed_limit_subscriber_);
std::this_thread::sleep_for(10ms);
}
}
bool TestNode::createSpeedFilter(const std::string & global_frame)
{
node_ = std::make_shared<nav2_util::LifecycleNode>("test_node");
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node_->get_clock());
tf_buffer_->setUsingDedicatedThread(true); // One-thread broadcasting-listening model
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
nav2_costmap_2d::LayeredCostmap layers(global_frame, false, false);
node_->declare_parameter(
std::string(FILTER_NAME) + ".transform_tolerance", rclcpp::ParameterValue(0.5));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".transform_tolerance", 0.5));
node_->declare_parameter(
std::string(FILTER_NAME) + ".filter_info_topic", rclcpp::ParameterValue(INFO_TOPIC));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".filter_info_topic", INFO_TOPIC));
node_->declare_parameter(
std::string(FILTER_NAME) + ".speed_limit_topic", rclcpp::ParameterValue(SPEED_LIMIT_TOPIC));
node_->set_parameter(
rclcpp::Parameter(std::string(FILTER_NAME) + ".speed_limit_topic", SPEED_LIMIT_TOPIC));
speed_filter_ = std::make_shared<nav2_costmap_2d::SpeedFilter>();
speed_filter_->initialize(&layers, FILTER_NAME, tf_buffer_.get(), node_, nullptr);
speed_filter_->initializeFilter(INFO_TOPIC);
speed_limit_subscriber_ = std::make_shared<SpeedLimitSubscriber>(SPEED_LIMIT_TOPIC);
// Wait until mask will be received by SpeedFilter
const std::chrono::nanoseconds timeout = 500ms;
rclcpp::Time start_time = node_->now();
while (!speed_filter_->isActive()) {
if (node_->now() - start_time > rclcpp::Duration(timeout)) {
return false;
}
rclcpp::spin_some(node_->get_node_base_interface());
std::this_thread::sleep_for(10ms);
}
return true;
}
void TestNode::createTFBroadcaster(const std::string & mask_frame, const std::string & global_frame)
{
tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(node_);
transform_ = std::make_unique<geometry_msgs::msg::TransformStamped>();
transform_->header.frame_id = mask_frame;
transform_->child_frame_id = global_frame;
transform_->header.stamp = node_->now() + rclcpp::Duration(100ms);
transform_->transform.translation.x = TRANSLATION_X;
transform_->transform.translation.y = TRANSLATION_Y;
transform_->transform.translation.z = 0.0;
transform_->transform.rotation.x = 0.0;
transform_->transform.rotation.y = 0.0;
transform_->transform.rotation.z = 0.0;
transform_->transform.rotation.w = 1.0;
tf_broadcaster_->sendTransform(*transform_);
// Allow tf_buffer_ to be filled by listener
waitSome(100ms);
}
void TestNode::publishTransform()
{
if (tf_broadcaster_) {
transform_->header.stamp = node_->now() + rclcpp::Duration(100ms);
tf_broadcaster_->sendTransform(*transform_);
}
}
void TestNode::verifySpeedLimit(
uint8_t type, double base, double multiplier,
unsigned int x, unsigned int y,
nav2_msgs::msg::SpeedLimit::SharedPtr speed_limit)
{
int8_t cost = mask_->makeData(x, y);
// expected_limit is being calculated by using float32 base and multiplier
double expected_limit = cost * multiplier + base;
if (type == nav2_costmap_2d::SPEED_FILTER_PERCENT) {
if (expected_limit < 0.0 || expected_limit > 100.0) {
expected_limit = nav2_costmap_2d::NO_SPEED_LIMIT;
}
EXPECT_TRUE(speed_limit->percentage);
EXPECT_TRUE(speed_limit->speed_limit >= 0.0);
EXPECT_TRUE(speed_limit->speed_limit <= 100.0);
EXPECT_NEAR(speed_limit->speed_limit, expected_limit, EPSILON);
} else if (type == nav2_costmap_2d::SPEED_FILTER_ABSOLUTE) {
if (expected_limit < 0.0) {
expected_limit = nav2_costmap_2d::NO_SPEED_LIMIT;
}
EXPECT_FALSE(speed_limit->percentage);
EXPECT_TRUE(speed_limit->speed_limit >= 0.0);
EXPECT_NEAR(speed_limit->speed_limit, expected_limit, EPSILON);
} else {
FAIL() << "The type of costmap filter is unknown";
}
}
void TestNode::testFullMask(
uint8_t type, double base, double multiplier,
double tr_x, double tr_y)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
nav2_msgs::msg::SpeedLimit::SharedPtr speed_limit;
// data = 0
pose.x = 1 - tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit == nullptr);
// data in range [1..100]
unsigned int x, y;
for (y = 1; y < height_; y++) {
for (x = 0; x < width_; x++) {
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
verifySpeedLimit(type, base, multiplier, x, y, speed_limit);
}
}
// data = 0
pose.x = 1 - tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
EXPECT_EQ(speed_limit->speed_limit, nav2_costmap_2d::NO_SPEED_LIMIT);
// data = -1
pose.x = -tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
EXPECT_EQ(speed_limit->speed_limit, nav2_costmap_2d::NO_SPEED_LIMIT);
}
void TestNode::testSimpleMask(
uint8_t type, double base, double multiplier,
double tr_x, double tr_y)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
nav2_msgs::msg::SpeedLimit::SharedPtr speed_limit;
// data = 0
pose.x = 1 - tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit == nullptr);
// data = <some_middle_value>
unsigned int x = width_ / 2 - 1;
unsigned int y = height_ / 2 - 1;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
verifySpeedLimit(type, base, multiplier, x, y, speed_limit);
// data = 100
x = width_ - 1;
y = height_ - 1;
pose.x = x - tr_x;
pose.y = y - tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
verifySpeedLimit(type, base, multiplier, x, y, speed_limit);
// data = 0
pose.x = 1 - tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
EXPECT_EQ(speed_limit->speed_limit, nav2_costmap_2d::NO_SPEED_LIMIT);
// data = -1
pose.x = -tr_x;
pose.y = -tr_y;
publishTransform();
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
EXPECT_EQ(speed_limit->speed_limit, nav2_costmap_2d::NO_SPEED_LIMIT);
}
void TestNode::testOutOfMask(uint8_t type, double base, double multiplier)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
nav2_msgs::msg::SpeedLimit::SharedPtr old_speed_limit, speed_limit;
// data = <some_middle_value>
pose.x = width_ / 2 - 1;
pose.y = height_ / 2 - 1;
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
old_speed_limit = waitSpeedLimit();
ASSERT_TRUE(old_speed_limit != nullptr);
verifySpeedLimit(type, base, multiplier, pose.x, pose.y, old_speed_limit);
// Then go to out of mask bounds and ensure that speed limit was not updated
pose.x = -2.0;
pose.y = -2.0;
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit == old_speed_limit);
pose.x = width_ + 1.0;
pose.y = height_ + 1.0;
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = getSpeedLimit();
ASSERT_TRUE(speed_limit == old_speed_limit);
}
void TestNode::testIncorrectLimits(uint8_t type, double base, double multiplier)
{
const int min_i = 0;
const int min_j = 0;
const int max_i = width_ + 4;
const int max_j = height_ + 4;
geometry_msgs::msg::Pose2D pose;
nav2_msgs::msg::SpeedLimit::SharedPtr speed_limit;
std::vector<std::tuple<unsigned int, unsigned int>> points;
// Some middle point corresponding to correct speed limit value
points.push_back(std::make_tuple(width_ / 2 - 1, height_ / 2 - 1));
// (0, 1) point corresponding to incorrect limit value: data = 1, value < 0
points.push_back(std::make_tuple(0, 1));
// Some middle point corresponding to correct speed limit value
points.push_back(std::make_tuple(width_ / 2 - 1, height_ / 2 - 1));
// (width_ - 1, height_ - 1) point corresponding to incorrect limit value:
// data = 100, value > 100
points.push_back(std::make_tuple(width_ - 1, height_ - 1));
for (auto it = points.begin(); it != points.end(); ++it) {
pose.x = static_cast<double>(std::get<0>(*it));
pose.y = static_cast<double>(std::get<1>(*it));
speed_filter_->process(*master_grid_, min_i, min_j, max_i, max_j, pose);
speed_limit = waitSpeedLimit();
ASSERT_TRUE(speed_limit != nullptr);
verifySpeedLimit(type, base, multiplier, pose.x, pose.y, speed_limit);
}
}
void TestNode::reset()
{
mask_.reset();
master_grid_.reset();
info_publisher_.reset();
mask_publisher_.reset();
speed_limit_subscriber_.reset();
speed_filter_.reset();
node_.reset();
tf_listener_.reset();
tf_broadcaster_.reset();
tf_buffer_.reset();
}
TEST_F(TestNode, testPercentSpeedLimit)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0);
EXPECT_TRUE(createSpeedFilter("map"));
// Test SpeedFilter
testFullMask(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testIncorrectPercentSpeedLimit)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_PERCENT, -50.0, 2.0);
EXPECT_TRUE(createSpeedFilter("map"));
// Test SpeedFilter
testIncorrectLimits(nav2_costmap_2d::SPEED_FILTER_PERCENT, -50.0, 2.0);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testAbsoluteSpeedLimit)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, 1.23, 4.5);
EXPECT_TRUE(createSpeedFilter("map"));
// Test SpeedFilter
testFullMask(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, 1.23, 4.5, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testIncorrectAbsoluteSpeedLimit)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, -50.0, 2.0);
EXPECT_TRUE(createSpeedFilter("map"));
// Test SpeedFilter
testIncorrectLimits(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, -50.0, 2.0);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testOutOfBounds)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0);
EXPECT_TRUE(createSpeedFilter("map"));
// Test SpeedFilter
testOutOfMask(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testInfoRePublish)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, 1.23, 4.5);
EXPECT_TRUE(createSpeedFilter("map"));
// Re-publish filter info (with incorrect base and multiplier)
// and test that everything is working after
rePublishInfo(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.1, 0.2);
// Test SpeedFilter
testSimpleMask(
nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.1, 0.2, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testMaskRePublish)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, 1.23, 4.5);
EXPECT_TRUE(createSpeedFilter("map"));
// Re-publish filter mask and test that everything is working after
rePublishMask();
// Test SpeedFilter
testSimpleMask(
nav2_costmap_2d::SPEED_FILTER_ABSOLUTE, 1.23, 4.5, NO_TRANSLATION, NO_TRANSLATION);
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testIncorrectFilterType)
{
// Initialize test system
createMaps("map");
publishMaps(INCORRECT_TYPE, 1.23, 4.5);
EXPECT_FALSE(createSpeedFilter("map"));
// Clean-up
speed_filter_->resetFilter();
reset();
}
TEST_F(TestNode, testDifferentFrame)
{
// Initialize test system
createMaps("map");
publishMaps(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0);
EXPECT_TRUE(createSpeedFilter("odom"));
createTFBroadcaster("map", "odom");
// Test SpeedFilter
testFullMask(nav2_costmap_2d::SPEED_FILTER_PERCENT, 0.0, 1.0, TRANSLATION_X, TRANSLATION_Y);
// Clean-up
speed_filter_->resetFilter();
reset();
}
int main(int argc, char ** argv)
{
// Initialize the system
testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
// Actual testing
bool test_result = RUN_ALL_TESTS();
// Shutdown
rclcpp::shutdown();
return test_result;
}