ROS2-SLAM-Fundamental/map_merge/src/map_merge.cpp
Carlos Andrés Álvarez Restrepo f808bc404a
PORT: multirobot_map_merge (#8)
* Baby steps

* Compile without implementations

* Compile combined_grids

* Compile part of principal node map_merge.cpp

* Finish 90% of main node

* Move ros::names implementation to own file

* Replace ros1 while loops threads with ros2 timers

* Fix checking of map type for adding robots. bypass mutex for now

* Working version without init poses, still verbose logs

* Add namespace to explore_lite
Add tb3 launch files for multirobot tests
Finish multirobot map merge with poses known

* Add files for tb3 demo

* Fix launch file for demo with robot poses

* Fix comments on locking

* Change deprecated Ptr to SharedPtr in ros2 msgs variables

* Change ConstPtr to ConstSharedPtr for ROS2

* Fix minor warnings in explor_lite

* Attempt fix slam toolbox

* Add launch and remove old ones

* Add copyright

* Add readme with instruction on how to run the demo in sim

* Fix readme and port from_map_server launch file

* Fix QoS for suscriptions with TRansientlocal
2021-12-26 20:28:18 -05:00

455 lines
17 KiB
C++

/*********************************************************************
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2014, Zhi Yan.
* Copyright (c) 2015-2016, Jiri Horner.
* Copyright (c) 2021, Carlos Alvarez.
* 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 Jiri Horner 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 <thread>
#include <map_merge/map_merge.h>
#include <map_merge/ros1_names.hpp>
#include <rcpputils/asserts.hpp>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
namespace map_merge
{
MapMerge::MapMerge() : Node("map_merge", rclcpp::NodeOptions()
.allow_undeclared_parameters(true)
.automatically_declare_parameters_from_overrides(true)),
subscriptions_size_(0)
{
std::string frame_id;
std::string merged_map_topic;
if (!this->has_parameter("merging_rate")) this->declare_parameter<double>("merging_rate", 4.0);
if (!this->has_parameter("discovery_rate")) this->declare_parameter<double>("discovery_rate", 0.05);
if (!this->has_parameter("estimation_rate")) this->declare_parameter<double>("estimation_rate", 0.5);
if (!this->has_parameter("known_init_poses")) this->declare_parameter<bool>("known_init_poses", true);
if (!this->has_parameter("estimation_confidence")) this->declare_parameter<double>("estimation_confidence", 1.0);
if (!this->has_parameter("robot_map_topic")) this->declare_parameter<std::string>("robot_map_topic", "map");
if (!this->has_parameter("robot_map_updates_topic")) this->declare_parameter<std::string>("robot_map_updates_topic", "map_updates");
if (!this->has_parameter("robot_namespace")) this->declare_parameter<std::string>("robot_namespace", "");
if (!this->has_parameter("merged_map_topic")) this->declare_parameter<std::string>("merged_map_topic", "map");
if (!this->has_parameter("world_frame")) this->declare_parameter<std::string>("world_frame", "world");
this->get_parameter("merging_rate", merging_rate_);
this->get_parameter("discovery_rate", discovery_rate_);
this->get_parameter("estimation_rate", estimation_rate_);
this->get_parameter("known_init_poses", have_initial_poses_);
this->get_parameter("estimation_confidence", confidence_threshold_);
this->get_parameter("robot_map_topic", robot_map_topic_);
this->get_parameter("robot_map_updates_topic", robot_map_updates_topic_);
this->get_parameter("robot_namespace", robot_namespace_);
this->get_parameter("merged_map_topic", merged_map_topic);
this->get_parameter("world_frame", world_frame_);
/* publishing */
// Create a publisher using the QoS settings to emulate a ROS1 latched topic
merged_map_publisher_ =
this->create_publisher<nav_msgs::msg::OccupancyGrid>(merged_map_topic,
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
// Timers
map_merging_timer_ = this->create_wall_timer(
std::chrono::milliseconds((uint16_t)(1000.0 / merging_rate_)),
[this]() { mapMerging(); });
// execute right away to simulate the ros1 first while loop on a thread
map_merging_timer_->execute_callback();
topic_subscribing_timer_ = this->create_wall_timer(
std::chrono::milliseconds((uint16_t)(1000.0 / discovery_rate_)),
[this]() { topicSubscribing(); });
// For topicSubscribing() we need to spin briefly for the discovery to happen
rclcpp::Rate r(100);
int i = 0;
while (rclcpp::ok() && i < 100) {
rclcpp::spin_some(this->get_node_base_interface());
r.sleep();
i++;
}
topic_subscribing_timer_->execute_callback();
if (!have_initial_poses_){
pose_estimation_timer_ = this->create_wall_timer(
std::chrono::milliseconds((uint16_t)(1000.0 / estimation_rate_)),
[this]() { poseEstimation(); });
// execute right away to simulate the ros1 first while loop on a thread
pose_estimation_timer_->execute_callback();
}
}
/*
* Subcribe to pose and map topics
*/
void MapMerge::topicSubscribing()
{
RCLCPP_DEBUG(logger_, "Robot discovery started.");
RCLCPP_INFO_ONCE(logger_, "Robot discovery started.");
// ros::master::V_TopicInfo topic_infos;
geometry_msgs::msg::Transform init_pose;
std::string robot_name;
std::string map_topic;
std::string map_updates_topic;
// ros::master::getTopics(topic_infos);
std::map<std::string, std::vector<std::string>> topic_infos = this->get_topic_names_and_types();
for (const auto& topic_it : topic_infos) {
std::string topic_name = topic_it.first;
std::vector<std::string> topic_types = topic_it.second;
// iterate over all topic types
for (const auto& topic_type : topic_types) {
// RCLCPP_INFO(logger_, "Topic: %s, type: %s", topic_name.c_str(), topic_type.c_str());
// we check only map topic
if (!isRobotMapTopic(topic_name, topic_type)) {
continue;
}
robot_name = robotNameFromTopic(topic_name);
if (robots_.count(robot_name)) {
// we already know this robot
continue;
}
if (have_initial_poses_ && !getInitPose(robot_name, init_pose)) {
RCLCPP_WARN(logger_, "Couldn't get initial position for robot [%s]\n"
"did you defined parameters map_merge/init_pose_[xyz]? in robot "
"namespace? If you want to run merging without known initial "
"positions of robots please set `known_init_poses` parameter "
"to false. See relevant documentation for details.",
robot_name.c_str());
continue;
}
RCLCPP_INFO(logger_, "adding robot [%s] to system", robot_name.c_str());
{
// We don't lock since because of ROS2 default executor only a callback can run at a time
// std::lock_guard<boost::shared_mutex> lock(subscriptions_mutex_);
subscriptions_.emplace_front();
++subscriptions_size_;
}
// no locking here. robots_ are used only in this procedure
MapSubscription& subscription = subscriptions_.front();
robots_.insert({robot_name, &subscription});
subscription.initial_pose = init_pose;
/* subscribe callbacks */
map_topic = ros1_names::append(robot_name, robot_map_topic_);
map_updates_topic =
ros1_names::append(robot_name, robot_map_updates_topic_);
RCLCPP_INFO(logger_, "Subscribing to MAP topic: %s.", map_topic.c_str());
auto map_qos = rclcpp::QoS(rclcpp::KeepLast(50)).transient_local().reliable();
subscription.map_sub = this->create_subscription<nav_msgs::msg::OccupancyGrid>(
map_topic, map_qos,
[this, &subscription](const nav_msgs::msg::OccupancyGrid::SharedPtr msg) {
fullMapUpdate(msg, subscription);
});
RCLCPP_INFO(logger_, "Subscribing to MAP updates topic: %s.",
map_updates_topic.c_str());
subscription.map_updates_sub =
this->create_subscription<map_msgs::msg::OccupancyGridUpdate>(
map_updates_topic, map_qos,
[this, &subscription](
const map_msgs::msg::OccupancyGridUpdate::SharedPtr msg) {
partialMapUpdate(msg, subscription);
});
}
}
}
/*
* mapMerging()
*/
void MapMerge::mapMerging()
{
RCLCPP_DEBUG(logger_, "Map merging started.");
RCLCPP_INFO_ONCE(logger_, "Map merging started.");
if (have_initial_poses_) {
// TODO: attempt fix for SLAM toolbox: add method for padding grids to same size
std::vector<nav_msgs::msg::OccupancyGrid::ConstSharedPtr> grids;
std::vector<geometry_msgs::msg::Transform> transforms;
grids.reserve(subscriptions_size_);
{
// We don't lock since because of ROS2 default executor only a callback can run
// boost::shared_lock<boost::shared_mutex> lock(subscriptions_mutex_);
for (auto& subscription : subscriptions_) {
// std::lock_guard<std::mutex> s_lock(subscription.mutex);
grids.push_back(subscription.readonly_map);
transforms.push_back(subscription.initial_pose);
}
}
// we don't need to lock here, because when have_initial_poses_ is true we
// will not run concurrently on the pipeline
pipeline_.feed(grids.begin(), grids.end());
pipeline_.setTransforms(transforms.begin(), transforms.end());
}
// nav_msgs::OccupancyGridPtr merged_map;
nav_msgs::msg::OccupancyGrid::SharedPtr merged_map;
{
std::lock_guard<std::mutex> lock(pipeline_mutex_);
merged_map = pipeline_.composeGrids();
}
if (!merged_map) {
// RCLCPP_INFO(logger_, "No map merged");
return;
}
RCLCPP_DEBUG(logger_, "all maps merged, publishing");
// RCLCPP_INFO(logger_, "all maps merged, publishing");
auto now = this->now();
merged_map->info.map_load_time = now;
merged_map->header.stamp = now;
merged_map->header.frame_id = world_frame_;
rcpputils::assert_true(merged_map->info.resolution > 0.f);
merged_map_publisher_->publish(*merged_map);
}
void MapMerge::poseEstimation()
{
RCLCPP_DEBUG(logger_, "Grid pose estimation started.");
RCLCPP_INFO_ONCE(logger_, "Grid pose estimation started.");
std::vector<nav_msgs::msg::OccupancyGrid::ConstSharedPtr> grids;
grids.reserve(subscriptions_size_);
{
// We don't lock since because of ROS2 default executor only a callback can run
// boost::shared_lock<boost::shared_mutex> lock(subscriptions_mutex_);
for (auto& subscription : subscriptions_) {
// std::lock_guard<std::mutex> s_lock(subscription.mutex);
grids.push_back(subscription.readonly_map);
}
}
// Print grids size
// RCLCPP_INFO(logger_, "Grids size: %d", grids.size());
std::lock_guard<std::mutex> lock(pipeline_mutex_);
pipeline_.feed(grids.begin(), grids.end());
// TODO allow user to change feature type
bool success = pipeline_.estimateTransforms(combine_grids::FeatureType::AKAZE,
confidence_threshold_);
// bool success = pipeline_.estimateTransforms(combine_grids::FeatureType::SURF,
// confidence_threshold_);
// bool success = pipeline_.estimateTransforms(combine_grids::FeatureType::ORB,
// confidence_threshold_);
if (!success) {
RCLCPP_INFO(logger_, "No grid poses estimated");
}
}
// void MapMerge::fullMapUpdate(const nav_msgs::OccupancyGrid::ConstPtr& msg,
// MapSubscription& subscription)
void MapMerge::fullMapUpdate(const nav_msgs::msg::OccupancyGrid::SharedPtr msg,
MapSubscription& subscription)
{
RCLCPP_DEBUG(logger_, "received full map update");
// RCLCPP_INFO(logger_, "received full map update");
std::lock_guard<std::mutex> lock(subscription.mutex);
if (subscription.readonly_map){
// ros2 header .stamp don't support > operator, we need to create them explicitly
auto t1 = rclcpp::Time(subscription.readonly_map->header.stamp);
auto t2 = rclcpp::Time(msg->header.stamp);
if (t1 > t2) {
// we have been overrunned by faster update. our work was useless.
return;
}
}
subscription.readonly_map = msg;
subscription.writable_map = nullptr;
}
// void MapMerge::partialMapUpdate(
// const map_msgs::OccupancyGridUpdate::ConstPtr& msg,
// MapSubscription& subscription)
void MapMerge::partialMapUpdate(const map_msgs::msg::OccupancyGridUpdate::SharedPtr msg,
MapSubscription& subscription)
{
RCLCPP_DEBUG(logger_, "received partial map update");
if (msg->x < 0 || msg->y < 0) {
RCLCPP_ERROR(logger_, "negative coordinates, invalid update. x: %d, y: %d", msg->x,
msg->y);
return;
}
size_t x0 = static_cast<size_t>(msg->x);
size_t y0 = static_cast<size_t>(msg->y);
size_t xn = msg->width + x0;
size_t yn = msg->height + y0;
nav_msgs::msg::OccupancyGrid::SharedPtr map;
nav_msgs::msg::OccupancyGrid::ConstSharedPtr readonly_map; // local copy
{
// load maps
std::lock_guard<std::mutex> lock(subscription.mutex);
map = subscription.writable_map;
readonly_map = subscription.readonly_map;
}
if (!readonly_map) {
RCLCPP_WARN(logger_, "received partial map update, but don't have any full map to "
"update. skipping.");
return;
}
// // we don't have partial map to take update, we must copy readonly map and
// // update new writable map
if (!map) {
map.reset(new nav_msgs::msg::OccupancyGrid(*readonly_map));
}
size_t grid_xn = map->info.width;
size_t grid_yn = map->info.height;
if (xn > grid_xn || x0 > grid_xn || yn > grid_yn || y0 > grid_yn) {
RCLCPP_WARN(logger_, "received update doesn't fully fit into existing map, "
"only part will be copied. received: [%lu, %lu], [%lu, %lu] "
"map is: [0, %lu], [0, %lu]",
x0, xn, y0, yn, grid_xn, grid_yn);
}
// update map with data
size_t i = 0;
for (size_t y = y0; y < yn && y < grid_yn; ++y) {
for (size_t x = x0; x < xn && x < grid_xn; ++x) {
size_t idx = y * grid_xn + x; // index to grid for this specified cell
map->data[idx] = msg->data[i];
++i;
}
}
// update time stamp
map->header.stamp = msg->header.stamp;
{
// store back updated map
std::lock_guard<std::mutex> lock(subscription.mutex);
if (subscription.readonly_map){
// ros2 header .stamp don't support > operator, we need to create them explicitly
auto t1 = rclcpp::Time(subscription.readonly_map->header.stamp);
auto t2 = rclcpp::Time(map->header.stamp);
if (t1 > t2) {
// we have been overrunned by faster update. our work was useless.
return;
}
}
subscription.writable_map = map;
subscription.readonly_map = map;
}
}
std::string MapMerge::robotNameFromTopic(const std::string& topic)
{
return ros1_names::parentNamespace(topic);
}
/* identifies topic via suffix */
bool MapMerge::isRobotMapTopic(const std::string topic, std::string type)
{
/* test whether topic is robot_map_topic_ */
std::string topic_namespace = ros1_names::parentNamespace(topic);
bool is_map_topic = ros1_names::append(topic_namespace, robot_map_topic_) == topic;
// /* test whether topic contains *anywhere* robot namespace */
auto pos = topic.find(robot_namespace_);
bool contains_robot_namespace = pos != std::string::npos;
// /* we support only occupancy grids as maps */
bool is_occupancy_grid = type == "nav_msgs/msg/OccupancyGrid";
// /* we don't want to subcribe on published merged map */
bool is_our_topic = merged_map_publisher_->get_topic_name() == topic;
return is_occupancy_grid && !is_our_topic && contains_robot_namespace &&
is_map_topic;
}
/*
* Get robot's initial position
*/
bool MapMerge::getInitPose(const std::string& name,
geometry_msgs::msg::Transform& pose)
{
std::string merging_namespace = ros1_names::append(name, "map_merge");
double yaw = 0.0;
bool success =
this->get_parameter(ros1_names::append(merging_namespace, "init_pose_x"),
pose.translation.x) &&
this->get_parameter(ros1_names::append(merging_namespace, "init_pose_y"),
pose.translation.y) &&
this->get_parameter(ros1_names::append(merging_namespace, "init_pose_z"),
pose.translation.z) &&
this->get_parameter(ros1_names::append(merging_namespace, "init_pose_yaw"),
yaw);
tf2::Quaternion q;
q.setEuler(0., 0., yaw);
pose.rotation = toMsg(q);
return success;
}
} // namespace map_merge
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
// ROS1 code
// // this package is still in development -- start wil debugging enabled
// if (ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME,
// ros::console::levels::Debug)) {
// ros::console::notifyLoggerLevelsChanged();
// }
// ROS2 code
auto node = std::make_shared<map_merge::MapMerge>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}