diff --git a/doc/parameters/param_list.md b/doc/parameters/param_list.md index aee685e6c88a74b6d5d53f4c12cf2d0fedd09952..f4a7089790c9c17da9af88d4959f085289f70622 100644 --- a/doc/parameters/param_list.md +++ b/doc/parameters/param_list.md @@ -51,11 +51,13 @@ For example: ```yaml local_costmap: ros__parameters: - plugins: ["obstacle_layer", "voxel_layer", "inflation_layer"] + plugins: ["obstacle_layer", "voxel_layer", "sonar_layer", "inflation_layer"] obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" voxel_layer: plugin: "nav2_costmap_2d::VoxelLayer" + sonar_layer: + plugin: "nav2_costmap_2d::RangeSensorLayer" inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" ``` @@ -115,7 +117,23 @@ When `plugins` parameter is not overridden, the following default plugins are lo | `<data source>`.marking | true | Whether source should mark in costmap | | `<data source>`.clearing | false | Whether source should raytrace clear in costmap | | `<data source>`.obstacle_range | 2.5 | Maximum range to mark obstacles in costmap | -| `<data source>`.raytrace_range | 3.0 | Maximum range to raytrace clear obstacles from costmap | +| `<data source>`.raytrace_range | 3.0 | Maximum range to raytrace clear obstacles from costmap | + +## range_sensor_layer plugin + +* `<range layer>`: Name corresponding to the `nav2_costmap_2d::RangeSensorLayer` plugin. This name gets defined in `plugin_names`. + +| Parameter | Default | Description | +| ----------| --------| ------------| +| `<range layer>`.enabled | true | Whether it is enabled | +| `<range layer>`.topics | [""] | Range topics to subscribe to | +| `<range layer>`.phi | 1.2 | Phi value | +| `<range layer>`.inflate_cone | 1.0 | Inflate the triangular area covered by the sensor (percentage) | +| `<range layer>`.no_readings_timeout | 0.0 | No Readings Timeout | +| `<range layer>`.clear_threshold | 0.2 | Probability below which cells are marked as free | +| `<range layer>`.mark_threshold | 0.8 | Probability above which cells are marked as occupied | +| `<range layer>`.clear_on_max_reading | false | Clear on max reading | +| `<range layer>`.input_sensor_type | ALL | Input sensor type either ALL (automatic selection), VARIABLE (min range != max range), or FIXED (min range == max range) | ## voxel_layer plugin diff --git a/nav2_costmap_2d/CMakeLists.txt b/nav2_costmap_2d/CMakeLists.txt index d7508415edff0898336a5dc532fa2d310d7278c7..4c9a2a93666e011cf4e4c222bb40c016ec9337c9 100644 --- a/nav2_costmap_2d/CMakeLists.txt +++ b/nav2_costmap_2d/CMakeLists.txt @@ -22,6 +22,7 @@ find_package(tf2 REQUIRED) find_package(tf2_ros REQUIRED) find_package(tf2_sensor_msgs REQUIRED) find_package(visualization_msgs REQUIRED) +find_package(angles REQUIRED) remove_definitions(-DDISABLE_LIBUSB-1.0) find_package(Eigen3 REQUIRED) @@ -72,6 +73,7 @@ set(dependencies tf2_ros tf2_sensor_msgs visualization_msgs + angles ) ament_target_dependencies(nav2_costmap_2d_core @@ -84,6 +86,7 @@ add_library(layers SHARED plugins/obstacle_layer.cpp src/observation_buffer.cpp plugins/voxel_layer.cpp + plugins/range_sensor_layer.cpp ) ament_target_dependencies(layers ${dependencies} diff --git a/nav2_costmap_2d/costmap_plugins.xml b/nav2_costmap_2d/costmap_plugins.xml index 62bfac732b959e935f4c1d8390ad208be313c2bd..a072539233252737c872ef6450ff93178e59d078 100644 --- a/nav2_costmap_2d/costmap_plugins.xml +++ b/nav2_costmap_2d/costmap_plugins.xml @@ -12,6 +12,9 @@ <class type="nav2_costmap_2d::VoxelLayer" base_class_type="nav2_costmap_2d::Layer"> <description>Similar to obstacle costmap, but uses 3D voxel grid to store data.</description> </class> + <class type="nav2_costmap_2d::RangeSensorLayer" base_class_type="nav2_costmap_2d::Layer"> + <description>A range-sensor (sonar, IR) based obstacle layer for costmap_2d</description> + </class> </library> </class_libraries> diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/range_sensor_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/range_sensor_layer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ce2861599d7a304d67c56e34bef0ec4261360695 --- /dev/null +++ b/nav2_costmap_2d/include/nav2_costmap_2d/range_sensor_layer.hpp @@ -0,0 +1,138 @@ +/* + * Software License Agreement (BSD License) + * + * Copyright (c) 2018 David V. Lu!! + * Copyright (c) 2020, Bytes Robotics + * 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 copyright holder 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 HOLDER 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. + */ + +#ifndef NAV2_COSTMAP_2D__RANGE_SENSOR_LAYER_HPP_ +#define NAV2_COSTMAP_2D__RANGE_SENSOR_LAYER_HPP_ + +#include <list> +#include <string> +#include <vector> +#include <mutex> + +#include "map_msgs/msg/occupancy_grid_update.hpp" +#include "message_filters/subscriber.h" +#include "nav2_costmap_2d/costmap_layer.hpp" +#include "nav2_costmap_2d/layered_costmap.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" +#include "rclcpp/rclcpp.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.h" +#include "sensor_msgs/msg/range.hpp" + +namespace nav2_costmap_2d +{ + +class RangeSensorLayer : public CostmapLayer +{ +public: + enum class InputSensorType + { + VARIABLE, + FIXED, + ALL + }; + + RangeSensorLayer(); + + virtual void onInitialize(); + virtual void updateBounds( + double robot_x, double robot_y, double robot_yaw, + double * min_x, double * min_y, double * max_x, double * max_y); + virtual void updateCosts( + nav2_costmap_2d::Costmap2D & master_grid, int min_i, + int min_j, int max_i, int max_j); + virtual void reset(); + virtual void deactivate(); + virtual void activate(); + + void bufferIncomingRangeMsg(const sensor_msgs::msg::Range::SharedPtr range_message); + +private: + void updateCostmap(); + void updateCostmap(sensor_msgs::msg::Range & range_message, bool clear_sensor_cone); + + void processRangeMsg(sensor_msgs::msg::Range & range_message); + void processFixedRangeMsg(sensor_msgs::msg::Range & range_message); + void processVariableRangeMsg(sensor_msgs::msg::Range & range_message); + + void resetRange(); + + inline double gamma(double theta); + inline double delta(double phi); + inline double sensor_model(double r, double phi, double theta); + + inline void get_deltas(double angle, double * dx, double * dy); + inline void update_cell( + double ox, double oy, double ot, + double r, double nx, double ny, bool clear); + + inline double to_prob(unsigned char c) + { + return static_cast<double>(c) / nav2_costmap_2d::LETHAL_OBSTACLE; + } + inline unsigned char to_cost(double p) + { + return static_cast<unsigned char>(p * nav2_costmap_2d::LETHAL_OBSTACLE); + } + + std::function<void(sensor_msgs::msg::Range & range_message)> processRangeMessageFunc_; + std::mutex range_message_mutex_; + std::list<sensor_msgs::msg::Range> range_msgs_buffer_; + + double max_angle_, phi_v_; + double inflate_cone_; + std::string global_frame_; + + double clear_threshold_, mark_threshold_; + bool clear_on_max_reading_; + + tf2::Duration transform_tolerance_; + double no_readings_timeout_; + rclcpp::Time last_reading_time_; + unsigned int buffered_readings_; + std::vector<rclcpp::Subscription<sensor_msgs::msg::Range>::SharedPtr> range_subs_; + double min_x_, min_y_, max_x_, max_y_; + + float area(int x1, int y1, int x2, int y2, int x3, int y3) + { + return fabs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0); + } + + int orient2d(int Ax, int Ay, int Bx, int By, int Cx, int Cy) + { + return (Bx - Ax) * (Cy - Ay) - (By - Ay) * (Cx - Ax); + } +}; +} // namespace nav2_costmap_2d +#endif // NAV2_COSTMAP_2D__RANGE_SENSOR_LAYER_HPP_ diff --git a/nav2_costmap_2d/plugins/inflation_layer.cpp b/nav2_costmap_2d/plugins/inflation_layer.cpp index 86561d334b3b0290f3ead3e45dfb905470884f58..df8405a3c91ba8b2f72e2528fc6f806a97eee3c2 100644 --- a/nav2_costmap_2d/plugins/inflation_layer.cpp +++ b/nav2_costmap_2d/plugins/inflation_layer.cpp @@ -40,6 +40,8 @@ #include <limits> #include <map> #include <vector> +#include <algorithm> +#include <utility> #include "nav2_costmap_2d/costmap_math.hpp" #include "nav2_costmap_2d/footprint.hpp" @@ -163,7 +165,7 @@ InflationLayer::updateCosts( } // make sure the inflation list is empty at the beginning of the cycle (should always be true) - for (auto & dist:inflation_cells_) { + for (auto & dist : inflation_cells_) { RCLCPP_FATAL_EXPRESSION( rclcpp::get_logger("nav2_costmap_2d"), !dist.empty(), "The inflation list must be empty at the beginning of inflation"); @@ -215,9 +217,10 @@ InflationLayer::updateCosts( // Process cells by increasing distance; new cells are appended to the // corresponding distance bin, so they // can overtake previously inserted but farther away cells - for (const auto & dist_bin: inflation_cells_) { + for (const auto & dist_bin : inflation_cells_) { for (std::size_t i = 0; i < dist_bin.size(); ++i) { - // Do not use iterator or for-range based loops to iterate though dist_bin, since it's size might + // Do not use iterator or for-range based loops to + // iterate though dist_bin, since it's size might // change when a new cell is enqueued, invalidating all iterators unsigned int index = dist_bin[i].index_; @@ -260,7 +263,7 @@ InflationLayer::updateCosts( } } - for (auto & dist:inflation_cells_) { + for (auto & dist : inflation_cells_) { dist.clear(); dist.reserve(200); } diff --git a/nav2_costmap_2d/plugins/range_sensor_layer.cpp b/nav2_costmap_2d/plugins/range_sensor_layer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8a6124a47a9282098485f6238aa855de9e5ff3c2 --- /dev/null +++ b/nav2_costmap_2d/plugins/range_sensor_layer.cpp @@ -0,0 +1,524 @@ +/* + * Software License Agreement (BSD License) + * + * Copyright (c) 2018 David V. Lu!! + * Copyright (c) 2020, Bytes Robotics + * 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 copyright holder 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 HOLDER 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 <angles/angles.h> +#include <algorithm> +#include <list> +#include <limits> +#include <string> +#include <vector> + +#include "pluginlib/class_list_macros.hpp" +#include "geometry_msgs/msg/point_stamped.hpp" +#include "nav2_costmap_2d/range_sensor_layer.hpp" + +PLUGINLIB_EXPORT_CLASS(nav2_costmap_2d::RangeSensorLayer, nav2_costmap_2d::Layer) + +using nav2_costmap_2d::LETHAL_OBSTACLE; +using nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE; +using nav2_costmap_2d::NO_INFORMATION; + +using namespace std::literals::chrono_literals; + +namespace nav2_costmap_2d +{ + +RangeSensorLayer::RangeSensorLayer() {} + +void RangeSensorLayer::onInitialize() +{ + current_ = true; + buffered_readings_ = 0; + last_reading_time_ = node_->now(); + default_value_ = to_cost(0.5); + + matchSize(); + resetRange(); + + declareParameter("enabled", rclcpp::ParameterValue(true)); + node_->get_parameter(name_ + "." + "enabled", enabled_); + declareParameter("phi", rclcpp::ParameterValue(1.2)); + node_->get_parameter(name_ + "." + "phi", phi_v_); + declareParameter("inflate_cone", rclcpp::ParameterValue(1.0)); + node_->get_parameter(name_ + "." + "phi", phi_v_); + declareParameter("no_readings_timeout", rclcpp::ParameterValue(0.0)); + node_->get_parameter(name_ + "." + "no_readings_timeout", no_readings_timeout_); + declareParameter("clear_threshold", rclcpp::ParameterValue(0.2)); + node_->get_parameter(name_ + "." + "clear_threshold", clear_threshold_); + declareParameter("mark_threshold", rclcpp::ParameterValue(0.8)); + node_->get_parameter(name_ + "." + "mark_threshold", mark_threshold_); + declareParameter("clear_on_max_reading", rclcpp::ParameterValue(false)); + node_->get_parameter(name_ + "." + "clear_on_max_reading", clear_on_max_reading_); + + double temp_tf_tol = 0.0; + node_->get_parameter("transform_tolerance", temp_tf_tol); + transform_tolerance_ = tf2::durationFromSec(temp_tf_tol); + + std::vector<std::string> topic_names{}; + declareParameter("topics", rclcpp::ParameterValue(topic_names)); + node_->get_parameter(name_ + "." + "topics", topic_names); + + InputSensorType input_sensor_type = InputSensorType::ALL; + std::string sensor_type_name; + declareParameter("input_sensor_type", rclcpp::ParameterValue("ALL")); + node_->get_parameter(name_ + "." + "input_sensor_type", sensor_type_name); + + std::transform( + sensor_type_name.begin(), sensor_type_name.end(), + sensor_type_name.begin(), ::toupper); + RCLCPP_INFO( + node_->get_logger(), "%s: %s as input_sensor_type given", + name_.c_str(), sensor_type_name.c_str()); + + if (sensor_type_name == "VARIABLE") { + input_sensor_type = InputSensorType::VARIABLE; + } else if (sensor_type_name == "FIXED") { + input_sensor_type = InputSensorType::FIXED; + } else if (sensor_type_name == "ALL") { + input_sensor_type = InputSensorType::ALL; + } else { + RCLCPP_ERROR( + node_->get_logger(), "%s: Invalid input sensor type: %s. Defaulting to ALL.", + name_.c_str(), sensor_type_name.c_str()); + } + + // Validate topic names list: it must be a (normally non-empty) list of strings + if (topic_names.empty()) { + RCLCPP_FATAL( + node_->get_logger(), "Invalid topic names list: it must" + "be a non-empty list of strings"); + return; + } + + // Traverse the topic names list subscribing to all of them with the same callback method + for (auto & topic_name : topic_names) { + if (input_sensor_type == InputSensorType::VARIABLE) { + processRangeMessageFunc_ = std::bind( + &RangeSensorLayer::processVariableRangeMsg, this, + std::placeholders::_1); + } else if (input_sensor_type == InputSensorType::FIXED) { + processRangeMessageFunc_ = std::bind( + &RangeSensorLayer::processFixedRangeMsg, this, + std::placeholders::_1); + } else if (input_sensor_type == InputSensorType::ALL) { + processRangeMessageFunc_ = std::bind( + &RangeSensorLayer::processRangeMsg, this, + std::placeholders::_1); + } else { + RCLCPP_ERROR( + node_->get_logger(), + "%s: Invalid input sensor type: %s. Did you make a new type" + "and forgot to choose the subscriber for it?", + name_.c_str(), sensor_type_name.c_str()); + } + range_subs_.push_back( + node_->create_subscription<sensor_msgs::msg::Range>( + topic_name, rclcpp::SensorDataQoS(), std::bind( + &RangeSensorLayer::bufferIncomingRangeMsg, this, + std::placeholders::_1))); + + RCLCPP_INFO( + node_->get_logger(), "RangeSensorLayer: subscribed to " + "topic %s", range_subs_.back()->get_topic_name()); + } + global_frame_ = layered_costmap_->getGlobalFrameID(); +} + + +double RangeSensorLayer::gamma(double theta) +{ + if (fabs(theta) > max_angle_) { + return 0.0; + } else { + return 1 - pow(theta / max_angle_, 2); + } +} + +double RangeSensorLayer::delta(double phi) +{ + return 1 - (1 + tanh(2 * (phi - phi_v_))) / 2; +} + +void RangeSensorLayer::get_deltas(double angle, double * dx, double * dy) +{ + double ta = tan(angle); + if (ta == 0) { + *dx = 0; + } else { + *dx = resolution_ / ta; + } + + *dx = copysign(*dx, cos(angle)); + *dy = copysign(resolution_, sin(angle)); +} + +double RangeSensorLayer::sensor_model(double r, double phi, double theta) +{ + double lbda = delta(phi) * gamma(theta); + + double delta = resolution_; + + if (phi >= 0.0 && phi < r - 2 * delta * r) { + return (1 - lbda) * (0.5); + } else if (phi < r - delta * r) { + return lbda * 0.5 * pow((phi - (r - 2 * delta * r)) / (delta * r), 2) + + (1 - lbda) * .5; + } else if (phi < r + delta * r) { + double J = (r - phi) / (delta * r); + return lbda * ((1 - (0.5) * pow(J, 2)) - 0.5) + 0.5; + } else { + return 0.5; + } +} + +void RangeSensorLayer::bufferIncomingRangeMsg( + const sensor_msgs::msg::Range::SharedPtr range_message) +{ + range_message_mutex_.lock(); + range_msgs_buffer_.push_back(*range_message); + range_message_mutex_.unlock(); +} + +void RangeSensorLayer::updateCostmap() +{ + std::list<sensor_msgs::msg::Range> range_msgs_buffer_copy; + + range_message_mutex_.lock(); + range_msgs_buffer_copy = std::list<sensor_msgs::msg::Range>(range_msgs_buffer_); + range_msgs_buffer_.clear(); + range_message_mutex_.unlock(); + + for (auto & range_msgs_it : range_msgs_buffer_copy) { + processRangeMessageFunc_(range_msgs_it); + } +} + +void RangeSensorLayer::processRangeMsg(sensor_msgs::msg::Range & range_message) +{ + if (range_message.min_range == range_message.max_range) { + processFixedRangeMsg(range_message); + } else { + processVariableRangeMsg(range_message); + } +} + +void RangeSensorLayer::processFixedRangeMsg(sensor_msgs::msg::Range & range_message) +{ + if (!std::isinf(range_message.range)) { + RCLCPP_ERROR( + node_->get_logger(), + "Fixed distance ranger (min_range == max_range) in frame %s sent invalid value. " + "Only -Inf (== object detected) and Inf (== no object detected) are valid.", + range_message.header.frame_id.c_str()); + return; + } + + bool clear_sensor_cone = false; + + if (range_message.range > 0) { // +inf + if (!clear_on_max_reading_) { + return; // no clearing at all + } + clear_sensor_cone = true; + } + + range_message.range = range_message.min_range; + + updateCostmap(range_message, clear_sensor_cone); +} + +void RangeSensorLayer::processVariableRangeMsg(sensor_msgs::msg::Range & range_message) +{ + if (range_message.range < range_message.min_range || range_message.range > + range_message.max_range) + { + return; + } + + bool clear_sensor_cone = false; + + if (range_message.range >= range_message.max_range && clear_on_max_reading_) { + clear_sensor_cone = true; + } + + updateCostmap(range_message, clear_sensor_cone); +} + +void RangeSensorLayer::updateCostmap( + sensor_msgs::msg::Range & range_message, + bool clear_sensor_cone) +{ + max_angle_ = range_message.field_of_view / 2; + + geometry_msgs::msg::PointStamped in, out; + in.header.stamp = range_message.header.stamp; + in.header.frame_id = range_message.header.frame_id; + + if (!tf_->canTransform( + in.header.frame_id, global_frame_, tf2_ros::fromMsg(in.header.stamp))) + { + RCLCPP_INFO( + node_->get_logger(), "Range sensor layer can't transform from %s to %s", + global_frame_.c_str(), in.header.frame_id.c_str()); + return; + } + + tf_->transform(in, out, global_frame_, transform_tolerance_); + + double ox = out.point.x, oy = out.point.y; + + in.point.x = range_message.range; + + tf_->transform(in, out, global_frame_, transform_tolerance_); + + double tx = out.point.x, ty = out.point.y; + + // calculate target props + double dx = tx - ox, dy = ty - oy, theta = atan2(dy, dx), d = sqrt(dx * dx + dy * dy); + + // Integer Bounds of Update + int bx0, by0, bx1, by1; + + // Triangle that will be really updated; the other cells within bounds are ignored + // This triangle is formed by the origin and left and right sides of sonar cone + int Ox, Oy, Ax, Ay, Bx, By; + + // Bounds includes the origin + worldToMapNoBounds(ox, oy, Ox, Oy); + bx1 = bx0 = Ox; + by1 = by0 = Oy; + touch(ox, oy, &min_x_, &min_y_, &max_x_, &max_y_); + + // Update Map with Target Point + unsigned int aa, ab; + if (worldToMap(tx, ty, aa, ab)) { + setCost(aa, ab, 233); + touch(tx, ty, &min_x_, &min_y_, &max_x_, &max_y_); + } + + double mx, my; + + // Update left side of sonar cone + mx = ox + cos(theta - max_angle_) * d * 1.2; + my = oy + sin(theta - max_angle_) * d * 1.2; + worldToMapNoBounds(mx, my, Ax, Ay); + bx0 = std::min(bx0, Ax); + bx1 = std::max(bx1, Ax); + by0 = std::min(by0, Ay); + by1 = std::max(by1, Ay); + touch(mx, my, &min_x_, &min_y_, &max_x_, &max_y_); + + // Update right side of sonar cone + mx = ox + cos(theta + max_angle_) * d * 1.2; + my = oy + sin(theta + max_angle_) * d * 1.2; + + worldToMapNoBounds(mx, my, Bx, By); + bx0 = std::min(bx0, Bx); + bx1 = std::max(bx1, Bx); + by0 = std::min(by0, By); + by1 = std::max(by1, By); + touch(mx, my, &min_x_, &min_y_, &max_x_, &max_y_); + + // Limit Bounds to Grid + bx0 = std::max(0, bx0); + by0 = std::max(0, by0); + bx1 = std::min(static_cast<int>(size_x_), bx1); + by1 = std::min(static_cast<int>(size_y_), by1); + + for (unsigned int x = bx0; x <= (unsigned int)bx1; x++) { + for (unsigned int y = by0; y <= (unsigned int)by1; y++) { + bool update_xy_cell = true; + + // Unless inflate_cone_ is set to 100 %, we update cells only within the + // (partially inflated) sensor cone, projected on the costmap as a triangle. + // 0 % corresponds to just the triangle, but if your sensor fov is very + // narrow, the covered area can become zero due to cell discretization. + // See wiki description for more details + if (inflate_cone_ < 1.0) { + // Determine barycentric coordinates + int w0 = orient2d(Ax, Ay, Bx, By, x, y); + int w1 = orient2d(Bx, By, Ox, Oy, x, y); + int w2 = orient2d(Ox, Oy, Ax, Ay, x, y); + + // Barycentric coordinates inside area threshold; this is not mathematically + // sound at all, but it works! + float bcciath = -static_cast<float>(inflate_cone_) * area(Ax, Ay, Bx, By, Ox, Oy); + update_xy_cell = w0 >= bcciath && w1 >= bcciath && w2 >= bcciath; + } + + if (update_xy_cell) { + double wx, wy; + mapToWorld(x, y, wx, wy); + update_cell(ox, oy, theta, range_message.range, wx, wy, clear_sensor_cone); + } + } + } + + buffered_readings_++; + last_reading_time_ = node_->now(); +} + +void RangeSensorLayer::update_cell( + double ox, double oy, double ot, double r, + double nx, double ny, bool clear) +{ + unsigned int x, y; + if (worldToMap(nx, ny, x, y)) { + double dx = nx - ox, dy = ny - oy; + double theta = atan2(dy, dx) - ot; + theta = angles::normalize_angle(theta); + double phi = sqrt(dx * dx + dy * dy); + double sensor = 0.0; + if (!clear) { + sensor = sensor_model(r, phi, theta); + } + double prior = to_prob(getCost(x, y)); + double prob_occ = sensor * prior; + double prob_not = (1 - sensor) * (1 - prior); + double new_prob = prob_occ / (prob_occ + prob_not); + + RCLCPP_DEBUG(node_->get_logger(), "%f %f | %f %f = %f", dx, dy, theta, phi, sensor); + RCLCPP_DEBUG(node_->get_logger(), "%f | %f %f | %f", prior, prob_occ, prob_not, new_prob); + unsigned char c = to_cost(new_prob); + setCost(x, y, c); + } +} + +void RangeSensorLayer::resetRange() +{ + min_x_ = min_y_ = std::numeric_limits<double>::max(); + max_x_ = max_y_ = -std::numeric_limits<double>::max(); +} + +void RangeSensorLayer::updateBounds( + double robot_x, double robot_y, + double robot_yaw, double * min_x, double * min_y, + double * max_x, double * max_y) +{ + robot_yaw = 0 + robot_yaw; // Avoid error if variable not in use + if (layered_costmap_->isRolling()) { + updateOrigin(robot_x - getSizeInMetersX() / 2, robot_y - getSizeInMetersY() / 2); + } + + updateCostmap(); + + *min_x = std::min(*min_x, min_x_); + *min_y = std::min(*min_y, min_y_); + *max_x = std::max(*max_x, max_x_); + *max_y = std::max(*max_y, max_y_); + + resetRange(); + + if (!enabled_) { + current_ = true; + return; + } + + if (buffered_readings_ == 0) { + if (no_readings_timeout_ > 0.0 && + (node_->now() - last_reading_time_).seconds() > no_readings_timeout_) + { + RCLCPP_WARN( + node_->get_logger(), "No range readings received for %.2f seconds, " + "while expected at least every %.2f seconds.", + (node_->now() - last_reading_time_).seconds(), + no_readings_timeout_); + current_ = false; + } + } +} + +void RangeSensorLayer::updateCosts( + nav2_costmap_2d::Costmap2D & master_grid, + int min_i, int min_j, int max_i, int max_j) +{ + if (!enabled_) { + return; + } + + unsigned char * master_array = master_grid.getCharMap(); + unsigned int span = master_grid.getSizeInCellsX(); + unsigned char clear = to_cost(clear_threshold_), mark = to_cost(mark_threshold_); + + for (int j = min_j; j < max_j; j++) { + unsigned int it = j * span + min_i; + for (int i = min_i; i < max_i; i++) { + unsigned char prob = costmap_[it]; + unsigned char current; + if (prob == nav2_costmap_2d::NO_INFORMATION) { + it++; + continue; + } else if (prob > mark) { + current = nav2_costmap_2d::LETHAL_OBSTACLE; + } else if (prob < clear) { + current = nav2_costmap_2d::FREE_SPACE; + } else { + it++; + continue; + } + + unsigned char old_cost = master_array[it]; + + if (old_cost == NO_INFORMATION || old_cost < current) { + master_array[it] = current; + } + it++; + } + } + + buffered_readings_ = 0; + current_ = true; +} + +void RangeSensorLayer::reset() +{ + RCLCPP_DEBUG(node_->get_logger(), "Reseting range sensor layer..."); + deactivate(); + resetMaps(); + current_ = true; + activate(); +} + +void RangeSensorLayer::deactivate() +{ + range_msgs_buffer_.clear(); +} + +void RangeSensorLayer::activate() +{ + range_msgs_buffer_.clear(); +} + +} // namespace nav2_costmap_2d diff --git a/nav2_costmap_2d/src/layered_costmap.cpp b/nav2_costmap_2d/src/layered_costmap.cpp index 48ba1ea467db00bfa8d887cfdac1aec699eae3b5..a03b79c0dcde4c08c323a647f2aeea2cb58f595d 100644 --- a/nav2_costmap_2d/src/layered_costmap.cpp +++ b/nav2_costmap_2d/src/layered_costmap.cpp @@ -42,6 +42,7 @@ #include <memory> #include <string> #include <vector> +#include <limits> #include "nav2_costmap_2d/footprint.hpp" diff --git a/nav2_costmap_2d/test/integration/CMakeLists.txt b/nav2_costmap_2d/test/integration/CMakeLists.txt index 4170ad3875b80bb246c6c8f30d9a2dc63cdd7700..5ecbeba923d69f06c2fe42af38342efe9c3c039a 100644 --- a/nav2_costmap_2d/test/integration/CMakeLists.txt +++ b/nav2_costmap_2d/test/integration/CMakeLists.txt @@ -43,6 +43,17 @@ target_link_libraries(obstacle_tests_exec layers ) +ament_add_gtest_executable(range_tests_exec + range_tests.cpp +) +ament_target_dependencies(range_tests_exec + ${dependencies} +) +target_link_libraries(range_tests_exec + nav2_costmap_2d_core + layers +) + ament_add_test(test_collision_checker GENERATE_RESULT_FOR_RETURN_CODE_ZERO COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/costmap_tests_launch.py" @@ -83,6 +94,16 @@ ament_add_test(obstacle_tests TEST_EXECUTABLE=$<TARGET_FILE:obstacle_tests_exec> ) +ament_add_test(range_tests + GENERATE_RESULT_FOR_RETURN_CODE_ZERO + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/costmap_tests_launch.py" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + ENV + TEST_MAP=${TEST_MAP_DIR}/TenByTen.yaml + TEST_LAUNCH_DIR=${TEST_LAUNCH_DIR} + TEST_EXECUTABLE=$<TARGET_FILE:range_tests_exec> +) + ## TODO(bpwilcox): this test (I believe) is intended to be launched with the simple_driving_test.xml, ## which has a dependency on rosbag playback # ament_add_gtest_executable(costmap_tester diff --git a/nav2_costmap_2d/test/integration/range_tests.cpp b/nav2_costmap_2d/test/integration/range_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..196406e7e023bf2460816289eb32d162f19dbb0c --- /dev/null +++ b/nav2_costmap_2d/test/integration/range_tests.cpp @@ -0,0 +1,293 @@ +/* + * Software License Agreement (BSD License) + * + * Copyright (c) 2020, Bytes Robotics + * 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 copyright holder 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 HOLDER 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 <memory> +#include <string> +#include <algorithm> +#include <utility> +#include <vector> + +#include "gtest/gtest.h" +#include "nav2_costmap_2d/costmap_2d.hpp" +#include "nav2_costmap_2d/layered_costmap.hpp" +#include "nav2_costmap_2d/observation_buffer.hpp" +#include "../testing_helper.hpp" +#include "sensor_msgs/msg/range.hpp" + +using std::begin; +using std::end; +using std::for_each; +using std::all_of; +using std::none_of; +using std::pair; +using std::string; + +class RclCppFixture +{ +public: + RclCppFixture() + { + rclcpp::init(0, nullptr); + } + + ~RclCppFixture() + { + rclcpp::shutdown(); + } +}; + +RclCppFixture g_rclcppfixture; + +class TestLifecycleNode : public nav2_util::LifecycleNode +{ +public: + explicit TestLifecycleNode(const string & name) + : nav2_util::LifecycleNode(name) + { + } + + nav2_util::CallbackReturn on_configure(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } + + nav2_util::CallbackReturn on_activate(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } + + nav2_util::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } + + nav2_util::CallbackReturn on_cleanup(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } + + nav2_util::CallbackReturn onShutdown(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } + + nav2_util::CallbackReturn onError(const rclcpp_lifecycle::State &) + { + return nav2_util::CallbackReturn::SUCCESS; + } +}; + +class TestNode : public ::testing::Test +{ +public: + TestNode() + : node_(std::make_shared<TestLifecycleNode>("range_test_node")), + tf_(node_->get_clock()) + { + // Standard non-plugin specific parameters + node_->declare_parameter("map_topic", rclcpp::ParameterValue(std::string("map"))); + node_->declare_parameter("track_unknown_space", rclcpp::ParameterValue(false)); + node_->declare_parameter("use_maximum", rclcpp::ParameterValue(false)); + node_->declare_parameter("lethal_cost_threshold", rclcpp::ParameterValue(100)); + node_->declare_parameter( + "unknown_cost_value", + rclcpp::ParameterValue(static_cast<unsigned char>(0xff))); + node_->declare_parameter("trinary_costmap", rclcpp::ParameterValue(true)); + node_->declare_parameter("transform_tolerance", rclcpp::ParameterValue(0.3)); + node_->declare_parameter("observation_sources", rclcpp::ParameterValue(std::string("range"))); + node_->declare_parameter("global_frame", rclcpp::ParameterValue(std::string("map"))); + + + // Range sensor specific parameters + node_->declare_parameter( + "range.topics", + rclcpp::ParameterValue( + std::vector<std::string>{"/range/topic"})); + node_->declare_parameter("range.phi", rclcpp::ParameterValue(1.2)); + node_->declare_parameter("range.clear_on_max_reading", rclcpp::ParameterValue(true)); + } + + ~TestNode() {} + +protected: + std::shared_ptr<TestLifecycleNode> node_; + tf2_ros::Buffer tf_; +}; + +// Test clearing at max range +TEST_F(TestNode, testClearingAtMaxRange) { + geometry_msgs::msg::TransformStamped transform; + transform.header.stamp = node_->now(); + transform.header.frame_id = "frame"; + transform.child_frame_id = "base_link"; + transform.transform.translation.y = 5; + transform.transform.translation.x = 2; + tf_.setTransform(transform, "default_authority", true); + + nav2_costmap_2d::LayeredCostmap layers("frame", false, false); + layers.resizeMap(10, 10, 1, 0, 0); + + std::shared_ptr<nav2_costmap_2d::RangeSensorLayer> rlayer{nullptr}; + addRangeLayer(layers, tf_, node_, rlayer); + + sensor_msgs::msg::Range msg; + msg.min_range = 1.0; + msg.max_range = 7.0; + msg.range = 2.0; + msg.header.stamp = node_->now(); + msg.header.frame_id = "base_link"; + msg.radiation_type = msg.ULTRASOUND; + msg.field_of_view = 0.174533; // 10 deg + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + ASSERT_EQ(layers.getCostmap()->getCost(4, 5), 254); + + msg.range = 7.0; + msg.header.stamp = node_->now(); + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + ASSERT_EQ(layers.getCostmap()->getCost(4, 5), 0); +} + +// Testing fixed scan with robot forward motion +TEST_F(TestNode, testProbabalisticModelForward) { + geometry_msgs::msg::TransformStamped transform; + transform.header.stamp = node_->now(); + transform.header.frame_id = "frame"; + transform.child_frame_id = "base_link"; + transform.transform.translation.y = 5; + transform.transform.translation.x = 2; + tf_.setTransform(transform, "default_authority", true); + + nav2_costmap_2d::LayeredCostmap layers("frame", false, false); + layers.resizeMap(10, 10, 1, 0, 0); + + std::shared_ptr<nav2_costmap_2d::RangeSensorLayer> rlayer{nullptr}; + addRangeLayer(layers, tf_, node_, rlayer); + + sensor_msgs::msg::Range msg; + msg.min_range = 1.0; + msg.max_range = 10.0; + msg.range = 3.0; + msg.header.stamp = node_->now(); + msg.header.frame_id = "base_link"; + msg.radiation_type = msg.ULTRASOUND; + msg.field_of_view = 0.174533; // 10 deg + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + transform.transform.translation.y = 5; + transform.transform.translation.x = 4; + tf_.setTransform(transform, "default_authority", true); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + transform.transform.translation.y = 5; + transform.transform.translation.x = 6; + tf_.setTransform(transform, "default_authority", true); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + ASSERT_EQ(layers.getCostmap()->getCost(5, 5), 254); + ASSERT_EQ(layers.getCostmap()->getCost(6, 5), 0); + ASSERT_EQ(layers.getCostmap()->getCost(7, 5), 254); + ASSERT_EQ(layers.getCostmap()->getCost(8, 5), 0); + ASSERT_EQ(layers.getCostmap()->getCost(9, 5), 254); +} + +// Testing fixed motion with downward movement +TEST_F(TestNode, testProbabalisticModelDownward) { + geometry_msgs::msg::TransformStamped transform; + transform.header.stamp = node_->now(); + transform.header.frame_id = "frame"; + transform.child_frame_id = "base_link"; + transform.transform.translation.y = 3; + transform.transform.translation.x = 2; + tf_.setTransform(transform, "default_authority", true); + + nav2_costmap_2d::LayeredCostmap layers("frame", false, false); + layers.resizeMap(10, 10, 1, 0, 0); + + std::shared_ptr<nav2_costmap_2d::RangeSensorLayer> rlayer{nullptr}; + addRangeLayer(layers, tf_, node_, rlayer); + + sensor_msgs::msg::Range msg; + msg.min_range = 1.0; + msg.max_range = 10.0; + msg.range = 1.0; + msg.header.stamp = node_->now(); + msg.header.frame_id = "base_link"; + msg.radiation_type = msg.ULTRASOUND; + msg.field_of_view = 0.174533; // 10 deg + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + transform.transform.translation.y = 5; + transform.transform.translation.x = 2; + tf_.setTransform(transform, "default_authority", true); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + rlayer->bufferIncomingRangeMsg(std::make_shared<sensor_msgs::msg::Range>(msg)); + + transform.transform.translation.y = 7; + transform.transform.translation.x = 2; + tf_.setTransform(transform, "default_authority", true); + + layers.updateMap(0, 0, 0); // 0, 0, 0 is robot pose +// printMap(*(layers.getCostmap())); + + ASSERT_EQ(layers.getCostmap()->getCost(3, 3), 254); + ASSERT_EQ(layers.getCostmap()->getCost(3, 4), 0); + ASSERT_EQ(layers.getCostmap()->getCost(3, 5), 254); + ASSERT_EQ(layers.getCostmap()->getCost(3, 6), 0); + ASSERT_EQ(layers.getCostmap()->getCost(3, 7), 254); +} diff --git a/nav2_costmap_2d/test/testing_helper.hpp b/nav2_costmap_2d/test/testing_helper.hpp index 013341adf9071ebee4cc019affca8c2a4ad112e3..82c7d53ec2bc5ad66739c119052b1bb0ca0bdf4f 100644 --- a/nav2_costmap_2d/test/testing_helper.hpp +++ b/nav2_costmap_2d/test/testing_helper.hpp @@ -23,6 +23,7 @@ #include "nav2_costmap_2d/cost_values.hpp" #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/static_layer.hpp" +#include "nav2_costmap_2d/range_sensor_layer.hpp" #include "nav2_costmap_2d/obstacle_layer.hpp" #include "nav2_costmap_2d/inflation_layer.hpp" #include "nav2_util/lifecycle_node.hpp" @@ -97,6 +98,16 @@ void addObstacleLayer( layers.addPlugin(std::shared_ptr<nav2_costmap_2d::Layer>(olayer)); } +void addRangeLayer( + nav2_costmap_2d::LayeredCostmap & layers, + tf2_ros::Buffer & tf, nav2_util::LifecycleNode::SharedPtr node, + std::shared_ptr<nav2_costmap_2d::RangeSensorLayer> & rlayer) +{ + rlayer = std::make_shared<nav2_costmap_2d::RangeSensorLayer>(); + rlayer->initialize(&layers, "range", &tf, node, nullptr, nullptr /*TODO*/); + layers.addPlugin(std::shared_ptr<nav2_costmap_2d::Layer>(rlayer)); +} + void addObservation( std::shared_ptr<nav2_costmap_2d::ObstacleLayer> olayer, double x, double y, double z = 0.0, double ox = 0.0, double oy = 0.0, double oz = MAX_Z, bool marking = true, bool clearing = true)