From 64ebf7c2cf36ac058470c8602bd2a7d48063e242 Mon Sep 17 00:00:00 2001 From: Yukihiro Saito Date: Thu, 17 Mar 2022 12:59:17 +0900 Subject: [PATCH 01/16] feat(multi_object_tracker): add iou gate (#483) * add iou gate Signed-off-by: Yukihiro Saito * ci(pre-commit): autofix * cosmetic change Signed-off-by: Yukihiro Saito * fix bug Signed-off-by: Yukihiro Saito * ci(pre-commit): autofix * fix bug Signed-off-by: Yukihiro Saito * fix tier4_launch Signed-off-by: Yukihiro Saito Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../data_association_matrix.param.yaml | 10 +++ .../config/data_association_matrix.param.yaml | 10 +++ .../data_association/data_association.hpp | 3 +- .../multi_object_tracker/utils/utils.hpp | 6 ++ .../src/data_association/data_association.cpp | 79 ++++++++++++------- .../src/multi_object_tracker_core.cpp | 4 +- .../multi_object_tracker/src/utils/utils.cpp | 69 ++++++++-------- 7 files changed, 112 insertions(+), 69 deletions(-) diff --git a/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml b/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml index 393bb949d048d..385eb335c3879 100644 --- a/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml +++ b/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml @@ -50,3 +50,13 @@ 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #PEDESTRIAN 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #ANIMAL + min_iou_matrix: + #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN + 0.1, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, #CAR + 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #TRUCK + 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #BUS + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #PEDESTRIAN + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #ANIMAL diff --git a/perception/multi_object_tracker/config/data_association_matrix.param.yaml b/perception/multi_object_tracker/config/data_association_matrix.param.yaml index 393bb949d048d..385eb335c3879 100644 --- a/perception/multi_object_tracker/config/data_association_matrix.param.yaml +++ b/perception/multi_object_tracker/config/data_association_matrix.param.yaml @@ -50,3 +50,13 @@ 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #PEDESTRIAN 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #ANIMAL + min_iou_matrix: + #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN + 0.1, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, #CAR + 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #TRUCK + 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #BUS + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #PEDESTRIAN + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #ANIMAL diff --git a/perception/multi_object_tracker/include/multi_object_tracker/data_association/data_association.hpp b/perception/multi_object_tracker/include/multi_object_tracker/data_association/data_association.hpp index 23feca474fe4f..a29fcdaa4e1e5 100644 --- a/perception/multi_object_tracker/include/multi_object_tracker/data_association/data_association.hpp +++ b/perception/multi_object_tracker/include/multi_object_tracker/data_association/data_association.hpp @@ -41,6 +41,7 @@ class DataAssociation Eigen::MatrixXd max_area_matrix_; Eigen::MatrixXd min_area_matrix_; Eigen::MatrixXd max_rad_matrix_; + Eigen::MatrixXd min_iou_matrix_; const double score_threshold_; std::unique_ptr gnn_solver_ptr_; @@ -49,7 +50,7 @@ class DataAssociation DataAssociation( std::vector can_assign_vector, std::vector max_dist_vector, std::vector max_area_vector, std::vector min_area_vector, - std::vector max_rad_vector); + std::vector max_rad_vector, std::vector min_iou_vector); void assign( const Eigen::MatrixXd & src, std::unordered_map & direct_assignment, std::unordered_map & reverse_assignment); diff --git a/perception/multi_object_tracker/include/multi_object_tracker/utils/utils.hpp b/perception/multi_object_tracker/include/multi_object_tracker/utils/utils.hpp index 00ba413700901..58ff17c0c9d68 100644 --- a/perception/multi_object_tracker/include/multi_object_tracker/utils/utils.hpp +++ b/perception/multi_object_tracker/include/multi_object_tracker/utils/utils.hpp @@ -28,6 +28,7 @@ #include #include +#include #include namespace utils @@ -36,6 +37,11 @@ double getPolygonArea(const geometry_msgs::msg::Polygon & footprint); double getRectangleArea(const geometry_msgs::msg::Vector3 & dimensions); double getCircleArea(const geometry_msgs::msg::Vector3 & dimensions); double getArea(const autoware_auto_perception_msgs::msg::Shape & shape); +double get2dIoU( + const std::tuple< + const geometry_msgs::msg::Pose &, const autoware_auto_perception_msgs::msg::Shape &> & object1, + const std::tuple< + const geometry_msgs::msg::Pose &, const autoware_auto_perception_msgs::msg::Shape &> & object2); double get2dIoU( const autoware_auto_perception_msgs::msg::TrackedObject & object1, const autoware_auto_perception_msgs::msg::TrackedObject & object2); diff --git a/perception/multi_object_tracker/src/data_association/data_association.cpp b/perception/multi_object_tracker/src/data_association/data_association.cpp index 42923a59c7f0b..4d3d83e20f3c0 100644 --- a/perception/multi_object_tracker/src/data_association/data_association.cpp +++ b/perception/multi_object_tracker/src/data_association/data_association.cpp @@ -79,7 +79,7 @@ double getFormedYawAngle( DataAssociation::DataAssociation( std::vector can_assign_vector, std::vector max_dist_vector, std::vector max_area_vector, std::vector min_area_vector, - std::vector max_rad_vector) + std::vector max_rad_vector, std::vector min_iou_vector) : score_threshold_(0.01) { { @@ -112,6 +112,12 @@ DataAssociation::DataAssociation( max_rad_vector.data(), max_rad_label_num, max_rad_label_num); max_rad_matrix_ = max_rad_matrix_tmp.transpose(); } + { + const int min_iou_label_num = static_cast(std::sqrt(min_iou_vector.size())); + Eigen::Map min_iou_matrix_tmp( + min_iou_vector.data(), min_iou_label_num, min_iou_label_num); + min_iou_matrix_ = min_iou_matrix_tmp.transpose(); + } gnn_solver_ptr_ = std::make_unique(); } @@ -167,42 +173,57 @@ Eigen::MatrixXd DataAssociation::calcScoreMatrix( utils::getHighestProbLabel(measurement_object.classification); double score = 0.0; - const geometry_msgs::msg::PoseWithCovariance tracker_pose_covariance = - (*tracker_itr)->getPoseWithCovariance(measurements.header.stamp); if (can_assign_matrix_(tracker_label, measurement_label)) { + autoware_auto_perception_msgs::msg::TrackedObject tracked_object; + (*tracker_itr)->getTrackedObject(measurements.header.stamp, tracked_object); + const double max_dist = max_dist_matrix_(tracker_label, measurement_label); - const double max_area = max_area_matrix_(tracker_label, measurement_label); - const double min_area = min_area_matrix_(tracker_label, measurement_label); - const double max_rad = max_rad_matrix_(tracker_label, measurement_label); const double dist = getDistance( measurement_object.kinematics.pose_with_covariance.pose.position, - tracker_pose_covariance.pose.position); - const double area = utils::getArea(measurement_object.shape); - score = (max_dist - std::min(dist, max_dist)) / max_dist; + tracked_object.kinematics.pose_with_covariance.pose.position); + bool passed_gate = true; // dist gate - if (max_dist < dist) { - score = 0.0; - // area gate - } else if (area < min_area || max_area < area) { - score = 0.0; - // angle gate - } else if (std::fabs(max_rad) < M_PI) { + if (passed_gate) { + if (max_dist < dist) passed_gate = false; + } + // area gate + if (passed_gate) { + const double max_area = max_area_matrix_(tracker_label, measurement_label); + const double min_area = min_area_matrix_(tracker_label, measurement_label); + const double area = utils::getArea(measurement_object.shape); + if (area < min_area || max_area < area) passed_gate = false; + } + // angle gate + if (passed_gate) { + const double max_rad = max_rad_matrix_(tracker_label, measurement_label); const double angle = getFormedYawAngle( measurement_object.kinematics.pose_with_covariance.pose.orientation, - tracker_pose_covariance.pose.orientation, false); - if (std::fabs(max_rad) < std::fabs(angle)) { - score = 0.0; - } - // mahalanobis dist gate - } else if (score < score_threshold_) { - double mahalanobis_dist = getMahalanobisDistance( - measurements.objects.at(measurement_idx).kinematics.pose_with_covariance.pose.position, - tracker_pose_covariance.pose.position, getXYCovariance(tracker_pose_covariance)); - - if (2.448 /*95%*/ <= mahalanobis_dist) { - score = 0.0; - } + tracked_object.kinematics.pose_with_covariance.pose.orientation, false); + if (std::fabs(max_rad) < M_PI && std::fabs(max_rad) < std::fabs(angle)) + passed_gate = false; + } + // mahalanobis dist gate + if (passed_gate) { + const double mahalanobis_dist = getMahalanobisDistance( + measurement_object.kinematics.pose_with_covariance.pose.position, + tracked_object.kinematics.pose_with_covariance.pose.position, + getXYCovariance(tracked_object.kinematics.pose_with_covariance)); + if (2.448 /*95%*/ <= mahalanobis_dist) passed_gate = false; + } + // 2d iou gate + if (passed_gate) { + const double min_iou = min_iou_matrix_(tracker_label, measurement_label); + const double iou = utils::get2dIoU( + {measurement_object.kinematics.pose_with_covariance.pose, measurement_object.shape}, + {tracked_object.kinematics.pose_with_covariance.pose, tracked_object.shape}); + if (iou < min_iou) passed_gate = false; + } + + // all gate is passed + if (passed_gate) { + score = (max_dist - std::min(dist, max_dist)) / max_dist; + if (score < score_threshold_) score = 0.0; } } score_matrix(tracker_idx, measurement_idx) = score; diff --git a/perception/multi_object_tracker/src/multi_object_tracker_core.cpp b/perception/multi_object_tracker/src/multi_object_tracker_core.cpp index 6ec99f44fa6ce..d5c2946843365 100644 --- a/perception/multi_object_tracker/src/multi_object_tracker_core.cpp +++ b/perception/multi_object_tracker/src/multi_object_tracker_core.cpp @@ -194,9 +194,11 @@ MultiObjectTracker::MultiObjectTracker(const rclcpp::NodeOptions & node_options) const auto max_area_matrix = this->declare_parameter>("max_area_matrix"); const auto min_area_matrix = this->declare_parameter>("min_area_matrix"); const auto max_rad_matrix = this->declare_parameter>("max_rad_matrix"); + const auto min_iou_matrix = this->declare_parameter>("min_iou_matrix"); data_association_ = std::make_unique( - can_assign_matrix, max_dist_matrix, max_area_matrix, min_area_matrix, max_rad_matrix); + can_assign_matrix, max_dist_matrix, max_area_matrix, min_area_matrix, max_rad_matrix, + min_iou_matrix); } void MultiObjectTracker::onMeasurement( diff --git a/perception/multi_object_tracker/src/utils/utils.cpp b/perception/multi_object_tracker/src/utils/utils.cpp index 79dfeb92359ea..556ffb8fcdc5f 100644 --- a/perception/multi_object_tracker/src/utils/utils.cpp +++ b/perception/multi_object_tracker/src/utils/utils.cpp @@ -11,10 +11,6 @@ // 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. -// -// -// Author: v1.0 Yukihiro Saito -// #include "multi_object_tracker/utils/utils.hpp" @@ -29,7 +25,7 @@ namespace utils { void toPolygon2d( - const autoware_auto_perception_msgs::msg::TrackedObject & object, + const geometry_msgs::msg::Pose & pose, const autoware_auto_perception_msgs::msg::Shape & shape, tier4_autoware_utils::Polygon2d & output); bool isClockWise(const tier4_autoware_utils::Polygon2d & polygon); tier4_autoware_utils::Polygon2d inverseClockWise(const tier4_autoware_utils::Polygon2d & polygon); @@ -51,8 +47,8 @@ double getPolygonArea(const geometry_msgs::msg::Polygon & footprint) { double area = 0.0; - for (int i = 0; i < static_cast(footprint.points.size()); ++i) { - int j = (i + 1) % static_cast(footprint.points.size()); + for (size_t i = 0; i < footprint.points.size(); ++i) { + size_t j = (i + 1) % footprint.points.size(); area += 0.5 * (footprint.points.at(i).x * footprint.points.at(j).y - footprint.points.at(j).x * footprint.points.at(i).y); } @@ -71,12 +67,14 @@ double getCircleArea(const geometry_msgs::msg::Vector3 & dimensions) } double get2dIoU( - const autoware_auto_perception_msgs::msg::TrackedObject & object1, - const autoware_auto_perception_msgs::msg::TrackedObject & object2) + const std::tuple< + const geometry_msgs::msg::Pose &, const autoware_auto_perception_msgs::msg::Shape &> & object1, + const std::tuple< + const geometry_msgs::msg::Pose &, const autoware_auto_perception_msgs::msg::Shape &> & object2) { tier4_autoware_utils::Polygon2d polygon1, polygon2; - toPolygon2d(object1, polygon1); - toPolygon2d(object2, polygon2); + toPolygon2d(std::get<0>(object1), std::get<1>(object1), polygon1); + toPolygon2d(std::get<0>(object2), std::get<1>(object2), polygon2); std::vector union_polygons; std::vector intersection_polygons; @@ -91,14 +89,19 @@ double get2dIoU( for (const auto & intersection_polygon : intersection_polygons) { intersection_area += boost::geometry::area(intersection_polygon); } - double iou; - if (union_area < 0.01) { - iou = 0.0f; - } - iou = std::min(1.0, intersection_area / union_area); + const double iou = union_area < 0.01 ? 0.0 : std::min(1.0, intersection_area / union_area); return iou; } +double get2dIoU( + const autoware_auto_perception_msgs::msg::TrackedObject & object1, + const autoware_auto_perception_msgs::msg::TrackedObject & object2) +{ + return get2dIoU( + {object1.kinematics.pose_with_covariance.pose, object1.shape}, + {object2.kinematics.pose_with_covariance.pose, object2.shape}); +} + tier4_autoware_utils::Polygon2d inverseClockWise(const tier4_autoware_utils::Polygon2d & polygon) { tier4_autoware_utils::Polygon2d inverted_polygon; @@ -124,23 +127,18 @@ bool isClockWise(const tier4_autoware_utils::Polygon2d & polygon) } void toPolygon2d( - const autoware_auto_perception_msgs::msg::TrackedObject & object, + const geometry_msgs::msg::Pose & pose, const autoware_auto_perception_msgs::msg::Shape & shape, tier4_autoware_utils::Polygon2d & output) { - if (object.shape.type == autoware_auto_perception_msgs::msg::Shape::BOUNDING_BOX) { - const auto & pose = object.kinematics.pose_with_covariance.pose; - double yaw = tier4_autoware_utils::normalizeRadian(tf2::getYaw(pose.orientation)); + if (shape.type == autoware_auto_perception_msgs::msg::Shape::BOUNDING_BOX) { + const double yaw = tier4_autoware_utils::normalizeRadian(tf2::getYaw(pose.orientation)); Eigen::Matrix2d rotation; rotation << std::cos(yaw), -std::sin(yaw), std::sin(yaw), std::cos(yaw); Eigen::Vector2d offset0, offset1, offset2, offset3; - offset0 = rotation * - Eigen::Vector2d(object.shape.dimensions.x * 0.5f, object.shape.dimensions.y * 0.5f); - offset1 = rotation * - Eigen::Vector2d(object.shape.dimensions.x * 0.5f, -object.shape.dimensions.y * 0.5f); - offset2 = rotation * - Eigen::Vector2d(-object.shape.dimensions.x * 0.5f, -object.shape.dimensions.y * 0.5f); - offset3 = rotation * - Eigen::Vector2d(-object.shape.dimensions.x * 0.5f, object.shape.dimensions.y * 0.5f); + offset0 = rotation * Eigen::Vector2d(shape.dimensions.x * 0.5f, shape.dimensions.y * 0.5f); + offset1 = rotation * Eigen::Vector2d(shape.dimensions.x * 0.5f, -shape.dimensions.y * 0.5f); + offset2 = rotation * Eigen::Vector2d(-shape.dimensions.x * 0.5f, -shape.dimensions.y * 0.5f); + offset3 = rotation * Eigen::Vector2d(-shape.dimensions.x * 0.5f, shape.dimensions.y * 0.5f); output.outer().push_back(boost::geometry::make( pose.position.x + offset0.x(), pose.position.y + offset0.y())); output.outer().push_back(boost::geometry::make( @@ -150,9 +148,9 @@ void toPolygon2d( output.outer().push_back(boost::geometry::make( pose.position.x + offset3.x(), pose.position.y + offset3.y())); output.outer().push_back(output.outer().front()); - } else if (object.shape.type == autoware_auto_perception_msgs::msg::Shape::CYLINDER) { - const auto & center = object.kinematics.pose_with_covariance.pose.position; - const auto & radius = object.shape.dimensions.x * 0.5; + } else if (shape.type == autoware_auto_perception_msgs::msg::Shape::CYLINDER) { + const auto & center = pose.position; + const auto & radius = shape.dimensions.x * 0.5; constexpr int n = 6; for (int i = 0; i < n; ++i) { Eigen::Vector2d point; @@ -170,13 +168,8 @@ void toPolygon2d( boost::geometry::make(point.x(), point.y())); } output.outer().push_back(output.outer().front()); - } else if (object.shape.type == autoware_auto_perception_msgs::msg::Shape::POLYGON) { - const auto & pose = object.kinematics.pose_with_covariance.pose; - // don't use yaw - // double yaw = tier4_autoware_utils::normalizeRadian(tf2::getYaw(pose.orientation)); - // Eigen::Matrix2d rotation; - // rotation << std::cos(yaw), -std::sin(yaw), std::sin(yaw), std::cos(yaw); - for (const auto & point : object.shape.footprint.points) { + } else if (shape.type == autoware_auto_perception_msgs::msg::Shape::POLYGON) { + for (const auto & point : shape.footprint.points) { output.outer().push_back(boost::geometry::make( pose.position.x + point.x, pose.position.y + point.y)); } From 99f7be258ccf5ef3d056e6756ed70987279dde0b Mon Sep 17 00:00:00 2001 From: Hiroki OTA Date: Thu, 17 Mar 2022 13:05:29 +0900 Subject: [PATCH 02/16] chore(dummy_diag_publisher): update README and launch file (#536) * chore: update README * feat: add param in launch * chore: Update system/dummy_diag_publisher/README.md Co-authored-by: Kenji Miyake <31987104+kenji-miyake@users.noreply.github.com> * chore: update readme * ci(pre-commit): autofix Co-authored-by: Kenji Miyake <31987104+kenji-miyake@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- system/dummy_diag_publisher/README.md | 11 ++++++----- .../launch/dummy_diag_publisher_node.launch.xml | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/system/dummy_diag_publisher/README.md b/system/dummy_diag_publisher/README.md index d553885f8ca70..0c424b9d0699d 100644 --- a/system/dummy_diag_publisher/README.md +++ b/system/dummy_diag_publisher/README.md @@ -16,11 +16,12 @@ This package outputs a dummy diagnostic data for debugging and developing. ### Node Parameters -| Name | Type | Default Value | Explanation | -| ------------- | ------ | ------------- | ------------------------------------- | -| `update_rate` | int | `10` | Timer callback period [Hz] | -| `diag_name` | string | `diag_name` | Diag_name set by dummy diag publisher | -| `is_active` | bool | `true` | Force update or not | +| Name | Type | Default Value | Explanation | Reconfigurable | +| ------------- | ------------------------------ | ------------- | --------------------------------------- | -------------- | +| `update_rate` | int | `10` | Timer callback period [Hz] | false | +| `diag_name` | string | `diag_name` | Diag_name set by dummy diag publisher | false | +| `is_active` | bool | `true` | Force update or not | true | +| `status` | DummyDiagPublisherNode::Status | `0 (OK)` | diag status set by dummy diag publisher | true | ## Assumptions / Known limits diff --git a/system/dummy_diag_publisher/launch/dummy_diag_publisher_node.launch.xml b/system/dummy_diag_publisher/launch/dummy_diag_publisher_node.launch.xml index 26fab9cf3ebd5..ae1042fc006bf 100644 --- a/system/dummy_diag_publisher/launch/dummy_diag_publisher_node.launch.xml +++ b/system/dummy_diag_publisher/launch/dummy_diag_publisher_node.launch.xml @@ -2,6 +2,7 @@ + @@ -10,6 +11,7 @@ + From d59bf262a091f5facfff8ea628f9f83a91e75ac0 Mon Sep 17 00:00:00 2001 From: Hiroki OTA Date: Thu, 17 Mar 2022 16:09:25 +0900 Subject: [PATCH 03/16] feat(system_error_monitor): add arg `extra_agg_config_file_system` to register user specific diag and delete tier4 specific diags (#537) * feat: add 'extra_agg_config_file_system' * feat: delete tier4 specific diag --- .../diagnostic_aggregator/system.param.yaml | 20 ------------------- .../launch/system_error_monitor.launch.xml | 2 ++ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/system/system_error_monitor/config/diagnostic_aggregator/system.param.yaml b/system/system_error_monitor/config/diagnostic_aggregator/system.param.yaml index 2cea6cc97cb3e..2cc4bca27724a 100644 --- a/system/system_error_monitor/config/diagnostic_aggregator/system.param.yaml +++ b/system/system_error_monitor/config/diagnostic_aggregator/system.param.yaml @@ -20,26 +20,6 @@ contains: [": emergency_stop_operation"] timeout: 1.0 - driving_recorder: - type: diagnostic_aggregator/AnalyzerGroup - path: driving_recorder - analyzers: - storage_error: - type: diagnostic_aggregator/GenericAnalyzer - path: storage_error - contains: ["bagkeeper"] - timeout: 3.0 - - debug_data_logger: - type: diagnostic_aggregator/AnalyzerGroup - path: debug_data_logger - analyzers: - storage_error: - type: diagnostic_aggregator/GenericAnalyzer - path: storage_error - contains: ["bagpacker"] - timeout: 3.0 - resource_monitoring: type: diagnostic_aggregator/AnalyzerGroup path: resource_monitoring diff --git a/system/system_error_monitor/launch/system_error_monitor.launch.xml b/system/system_error_monitor/launch/system_error_monitor.launch.xml index 981fc8395a545..c35761d994262 100644 --- a/system/system_error_monitor/launch/system_error_monitor.launch.xml +++ b/system/system_error_monitor/launch/system_error_monitor.launch.xml @@ -17,6 +17,7 @@ + @@ -34,6 +35,7 @@ + From 3ec81b558d3c3b201c9b369b2f1b1ebf2f4a7e66 Mon Sep 17 00:00:00 2001 From: Kenji Miyake <31987104+kenji-miyake@users.noreply.github.com> Date: Thu, 17 Mar 2022 19:12:52 +0900 Subject: [PATCH 04/16] chore: sync issue and PR templates (#376) * chore: sync issue and PR templates Signed-off-by: Kenji Miyake * Update sync-files.yaml --- .github/sync-files.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/sync-files.yaml b/.github/sync-files.yaml index 614496c01d6cc..e5cd1f601bfed 100644 --- a/.github/sync-files.yaml +++ b/.github/sync-files.yaml @@ -4,6 +4,12 @@ - source: CONTRIBUTING.md - source: DISCLAIMER.md - source: LICENSE + - source: .github/ISSUE_TEMPLATE/bug.yaml + - source: .github/ISSUE_TEMPLATE/config.yml + - source: .github/ISSUE_TEMPLATE/task.yaml + - source: .github/PULL_REQUEST_TEMPLATE.md + - source: .github/PULL_REQUEST_TEMPLATE/small-change.md + - source: .github/PULL_REQUEST_TEMPLATE/standard-change.md - source: .github/dependabot.yaml - source: .github/workflows/pre-commit.yaml - source: .github/workflows/pre-commit-optional.yaml From b62a02279da2fc6320e61e66d9483f0dbf8db375 Mon Sep 17 00:00:00 2001 From: "awf-autoware-bot[bot]" <94889083+awf-autoware-bot[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 10:18:29 +0000 Subject: [PATCH 05/16] chore: sync files (#540) Signed-off-by: GitHub Co-authored-by: kenji-miyake --- .github/ISSUE_TEMPLATE/bug.yaml | 71 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 13 ++++ .github/ISSUE_TEMPLATE/task.yaml | 42 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 51 ++----------- .github/PULL_REQUEST_TEMPLATE/small-change.md | 27 +++++++ .../PULL_REQUEST_TEMPLATE/standard-change.md | 42 +++++++++++ 6 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/task.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE/small-change.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/standard-change.md diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000000000..12a857998a0b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,71 @@ +name: Bug +description: Report a bug +body: + - type: checkboxes + attributes: + label: Checklist + description: Confirm the following items before proceeding. If one cannot be satisfied, create a discussion thread instead. + options: + - label: I've read the [contribution guidelines](https://github.com/autowarefoundation/autoware/blob/main/CONTRIBUTING.md). + required: true + - label: I've searched other issues and no duplicate issues were found. + required: true + - label: I'm convinced that this is not my fault but a bug. + required: true + + - type: textarea + attributes: + label: Description + description: Write a brief description of the bug. + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: Describe the expected behavior. + validations: + required: true + + - type: textarea + attributes: + label: Actual behavior + description: Describe the actual behavior. + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: Write the steps to reproduce the bug. + placeholder: |- + 1. + 2. + 3. + validations: + required: true + + - type: textarea + attributes: + label: Versions + description: Provide the version information. You can omit this if you believe it's irrelevant. + placeholder: |- + - OS: + - ROS 2: + - Autoware: + validations: + required: false + + - type: textarea + attributes: + label: Possible causes + description: Write the possible causes if you have any ideas. + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other additional context if it exists. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..48765c24a7b25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://github.com/autowarefoundation/autoware/discussions/new?category=q-a + about: Ask a question + + - name: Feature request + url: https://github.com/autowarefoundation/autoware/discussions/new?category=feature-requests + about: Send a feature request + + - name: Idea + url: https://github.com/autowarefoundation/autoware/discussions/new?category=ideas + about: Post an idea diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 0000000000000..cd8322f507405 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,42 @@ +name: Task +description: Plan a task +body: + - type: checkboxes + attributes: + label: Checklist + description: Confirm the following items before proceeding. If one cannot be satisfied, create a discussion thread instead. + options: + - label: I've read the [contribution guidelines](https://github.com/autowarefoundation/autoware/blob/main/CONTRIBUTING.md). + required: true + - label: I've searched other issues and no duplicate issues were found. + required: true + - label: I've agreed with the maintainers that I can plan this task. + required: true + + - type: textarea + attributes: + label: Description + description: Write a brief description of the task. + validations: + required: true + + - type: textarea + attributes: + label: Purpose + description: Describe the purpose of the task. + validations: + required: true + + - type: textarea + attributes: + label: Possible approaches + description: Describe possible approaches for the task. + validations: + required: true + + - type: textarea + attributes: + label: Definition of done + description: Write the definition of done for the task. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 42c750dd7d811..14776dc9f032c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,49 +1,6 @@ -## Related Issue(required) +**Note**: Confirm our [contribution guidelines](https://autowarefoundation.github.io/autoware-documentation/main/contributing/) before submitting a pull request. - +Click the `Preview` tab and select a PR template: -## Description(required) - - - -## Review Procedure(required) - - - -## Related PR(optional) - - - -## Pre-Review Checklist for the PR Author - -**PR Author should check the checkboxes below when creating the PR.** - -- [ ] Read [commit-guidelines][commit-guidelines] -- [ ] Assign PR to reviewer - -If you are adding new package following items are required: - -- [ ] Documentation with description of the package is available -- [ ] A sample launch file and parameter file are available if the package contains executable nodes - -## Checklist for the PR Reviewer - -**Reviewers should check the checkboxes below before approval.** - -- [ ] Commits are properly organized and messages are according to the guideline -- [ ] PR title describes the changes - -## Post-Review Checklist for the PR Author - -**PR Author should check the checkboxes below before merging.** - -- [ ] All open points are addressed and tracked via issues or tickets - -## CI Checks - -- **Build and test for PR / build-and-test-pr**: Required to pass before the merge. -- **Build and test for PR / clang-tidy-pr**: NOT required to pass before the merge. It is up to the reviewer(s). Found false positives? See the [guidelines][clang-tidy-guidelines]. -- **Check spelling**: NOT required to pass before the merge. It is up to the reviewer(s). See [here][spell-check-dict] if you want to add some words to the spell check dictionary. - -[commit-guidelines]: https://www.conventionalcommits.org/en/v1.0.0/ -[spell-check-dict]: https://github.com/tier4/autoware-spell-check-dict#how-to-contribute +- [Standard change](?expand=1&template=standard-change.md) +- [Small change](?expand=1&template=small-change.md) diff --git a/.github/PULL_REQUEST_TEMPLATE/small-change.md b/.github/PULL_REQUEST_TEMPLATE/small-change.md new file mode 100644 index 0000000000000..f8ef7e75d2cfc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/small-change.md @@ -0,0 +1,27 @@ +## Description + + + +## Pre-review checklist for the PR author + +PR author **must** check the checkboxes below when creating the PR. + +- [ ] I've confirmed the [contribution guidelines]. +- [ ] The PR follows the [pull request guidelines]. + +## In-review checklist for the PR reviewers + +Reviewers **must** check the checkboxes below before approval. + +- [ ] The PR follows the [pull request guidelines]. + +## Post-review checklist for the PR author + +PR author **must** check the checkboxes below before merging. + +- [ ] There are no open discussions or they are tracked via tickets. + +After all checkboxes are checked, anyone who has the write access can merge the PR. + +[contribution guidelines]: https://autowarefoundation.github.io/autoware-documentation/main/contributing/ +[pull request guidelines]: https://autowarefoundation.github.io/autoware-documentation/main/contributing/pull-request-guidelines/ diff --git a/.github/PULL_REQUEST_TEMPLATE/standard-change.md b/.github/PULL_REQUEST_TEMPLATE/standard-change.md new file mode 100644 index 0000000000000..467a2fc7c780a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/standard-change.md @@ -0,0 +1,42 @@ +## Description + + + +## Related links + + + +## Tests performed + + + +## Notes for reviewers + + + +## Pre-review checklist for the PR author + +PR author **must** check the checkboxes below when creating the PR. + +- [ ] I've confirmed the [contribution guidelines]. +- [ ] The PR follows the [pull request guidelines]. + +## In-review checklist for the PR reviewers + +Reviewers **must** check the checkboxes below before approval. + +- [ ] The PR follows the [pull request guidelines]. +- [ ] The PR has been properly tested. +- [ ] The PR has been reviewed by the code owners. + +## Post-review checklist for the PR author + +PR author **must** check the checkboxes below before merging. + +- [ ] There are no open discussions or they are tracked via tickets. +- [ ] The PR is ready for merge. + +After all checkboxes are checked, anyone who has the write access can merge the PR. + +[contribution guidelines]: https://autowarefoundation.github.io/autoware-documentation/main/contributing/ +[pull request guidelines]: https://autowarefoundation.github.io/autoware-documentation/main/contributing/pull-request-guidelines/ From a67955dac4eb7c8809256efe150afd00981a04e9 Mon Sep 17 00:00:00 2001 From: Satoshi OTA <44889564+satoshi-ota@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:44:18 +0900 Subject: [PATCH 06/16] feat(tier4_perception_rviz_plugin): interactive pedestrian for rviz plugin (#534) * feat(tier4_perception_rviz_plugin): add mouse event util function Signed-off-by: satoshi-ota * feat(tier4_perception_rviz_plugin): delete option for QT Signed-off-by: satoshi-ota * fix(tier4_perception_rviz_plugin): remove interactive_pedestrian.cpp Signed-off-by: satoshi-ota * feat(tier4_perception_rviz_plugin): expand conventional 2D dummy pedestrian to be able to interact Signed-off-by: satoshi-ota * fix(tier4_perception_rviz_plugin): change for void Signed-off-by: satoshi-ota * fix(tier4_perception_rviz_plugin): not use std::numeric_limits::epsilon() Signed-off-by: satoshi-ota --- .../CMakeLists.txt | 2 +- .../src/tools/pedestrian_pose.cpp | 285 +++++++++++++++++- .../src/tools/pedestrian_pose.hpp | 66 ++++ .../src/tools/util.cpp | 37 +++ .../src/tools/util.hpp | 26 ++ 5 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 common/tier4_perception_rviz_plugin/src/tools/util.cpp create mode 100644 common/tier4_perception_rviz_plugin/src/tools/util.hpp diff --git a/common/tier4_perception_rviz_plugin/CMakeLists.txt b/common/tier4_perception_rviz_plugin/CMakeLists.txt index a262c0bf0840f..2bac295514315 100644 --- a/common/tier4_perception_rviz_plugin/CMakeLists.txt +++ b/common/tier4_perception_rviz_plugin/CMakeLists.txt @@ -18,10 +18,10 @@ set(QT_LIBRARIES Qt5::Widgets) set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) -add_definitions(-DQT_NO_KEYWORDS) ## Declare a C++ library ament_auto_add_library(tier4_perception_rviz_plugin SHARED + src/tools/util.cpp src/tools/pedestrian_pose.cpp src/tools/car_pose.cpp src/tools/unknown_pose.cpp diff --git a/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.cpp b/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.cpp index 85b91aeb64b41..cac7e6ac72f7e 100644 --- a/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.cpp +++ b/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.cpp @@ -47,25 +47,188 @@ #include "pedestrian_pose.hpp" -#include -#include -#include - -#include +#include "util.hpp" -#include -#include +#include #include +#include #include #include namespace rviz_plugins { +InteractivePedestrian::InteractivePedestrian(const Ogre::Vector3 & point) +{ + velocity_ = Ogre::Vector3::ZERO; + point_ = point; + theta_ = 0.0; + + std::mt19937 gen(std::random_device{}()); + std::independent_bits_engine bit_eng(gen); + std::generate(uuid_.begin(), uuid_.end(), bit_eng); +} + +std::array InteractivePedestrian::uuid() const { return uuid_; } + +void InteractivePedestrian::twist(geometry_msgs::msg::Twist & twist) const +{ + twist.linear.x = velocity_.length(); + twist.linear.y = 0.0; + twist.linear.z = 0.0; +} + +void InteractivePedestrian::transform(tf2::Transform & tf_map2object) const +{ + tf2::Transform tf_object_origin2moved_object; + tf2::Transform tf_map2object_origin; + + { + geometry_msgs::msg::Transform ros_object_origin2moved_object; + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, theta_); + ros_object_origin2moved_object.rotation = tf2::toMsg(quat); + tf2::fromMsg(ros_object_origin2moved_object, tf_object_origin2moved_object); + } + + { + geometry_msgs::msg::Transform ros_object_map2object_origin; + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, 0.0); + ros_object_map2object_origin.rotation = tf2::toMsg(quat); + ros_object_map2object_origin.translation.x = point_.x; + ros_object_map2object_origin.translation.y = point_.y; + ros_object_map2object_origin.translation.z = point_.z; + tf2::fromMsg(ros_object_map2object_origin, tf_map2object_origin); + } + + tf_map2object = tf_map2object_origin * tf_object_origin2moved_object; +} + +void InteractivePedestrian::update(const Ogre::Vector3 & point) +{ + velocity_ = point - point_; + point_ = point; + theta_ = + (velocity_.x < 1.0e-3 && velocity_.y < 1.0e-3) ? theta_ : std::atan2(velocity_.y, velocity_.x); +} + +double InteractivePedestrian::distance(const Ogre::Vector3 & point) +{ + return point_.distance(point); +} + +InteractivePedestrianCollection::InteractivePedestrianCollection() { target_ = nullptr; } + +void InteractivePedestrianCollection::reset() { target_ = nullptr; } + +void InteractivePedestrianCollection::select(const Ogre::Vector3 & point) +{ + const size_t index = nearest(point); + if (index != objects_.size()) { + target_ = objects_[index].get(); + } +} + +boost::optional> InteractivePedestrianCollection::create( + const Ogre::Vector3 & point) +{ + objects_.emplace_back(std::make_unique(point)); + target_ = objects_.back().get(); + + if (target_) { + return target_->uuid(); + } + + return {}; +} + +boost::optional> InteractivePedestrianCollection::remove( + const Ogre::Vector3 & point) +{ + const size_t index = nearest(point); + if (index != objects_.size()) { + const auto removed_uuid = objects_[index].get()->uuid(); + objects_.erase(objects_.begin() + index); + return removed_uuid; + } + + return {}; +} + +boost::optional> InteractivePedestrianCollection::update( + const Ogre::Vector3 & point) +{ + if (target_) { + target_->update(point); + return target_->uuid(); + } + + return {}; +} + +boost::optional InteractivePedestrianCollection::twist( + const std::array & uuid) const +{ + for (const auto & object : objects_) { + if (object->uuid() != uuid) { + continue; + } + + geometry_msgs::msg::Twist ret; + object->twist(ret); + return ret; + } + + return {}; +} + +boost::optional InteractivePedestrianCollection::transform( + const std::array & uuid) const +{ + for (const auto & object : objects_) { + if (object->uuid() != uuid) { + continue; + } + + tf2::Transform ret; + object->transform(ret); + return ret; + } + + return {}; +} + +size_t InteractivePedestrianCollection::nearest(const Ogre::Vector3 & point) +{ + const size_t npos = objects_.size(); + if (objects_.empty()) { + return npos; + } + + std::vector distances; + for (const auto & object : objects_) { + distances.push_back(object->distance(point)); + } + + const auto compare = [&](int x, int y) { return distances[x] < distances[y]; }; + std::vector indices(distances.size()); + std::iota(indices.begin(), indices.end(), 0); + std::sort(indices.begin(), indices.end(), compare); + + const size_t index = indices[0]; + return distances[index] < 2.0 ? index : npos; +} PedestrianInitialPoseTool::PedestrianInitialPoseTool() { shortcut_key_ = 'l'; + enable_interactive_property_ = new rviz_common::properties::BoolProperty( + "Interactive", false, "Enable/Disable interactive action by manipulating mouse.", + getPropertyContainer()); + property_frame_ = new rviz_common::properties::TfFrameProperty( + "Target Frame", rviz_common::properties::TfFrameProperty::FIXED_FRAME_STRING, + "The TF frame where the point cloud is output.", getPropertyContainer(), nullptr, true); topic_property_ = new rviz_common::properties::StringProperty( "Pose Topic", "/simulation/dummy_perception_publisher/object_info", "The topic on which to publish dummy object info.", getPropertyContainer(), SLOT(updateTopic()), @@ -103,10 +266,16 @@ void PedestrianInitialPoseTool::updateTopic() dummy_object_info_pub_ = raw_node->create_publisher( topic_property_->getStdString(), 1); clock_ = raw_node->get_clock(); + move_tool_.initialize(context_); + property_frame_->setFrameManager(context_->getFrameManager()); } void PedestrianInitialPoseTool::onPoseSet(double x, double y, double theta) { + if (enable_interactive_property_->getBool()) { + return; + } + dummy_perception_publisher::msg::Object output_msg; std::string fixed_frame = context_->getFixedFrame().toStdString(); @@ -165,6 +334,108 @@ void PedestrianInitialPoseTool::onPoseSet(double x, double y, double theta) dummy_object_info_pub_->publish(output_msg); } +void PedestrianInitialPoseTool::publishObjectMsg( + const std::array & uuid, const uint32_t action) +{ + Object output_msg; + std::string fixed_frame = context_->getFixedFrame().toStdString(); + + // header + output_msg.header.frame_id = fixed_frame; + output_msg.header.stamp = clock_->now(); + + // action + output_msg.action = action; + + // id + output_msg.id.uuid = uuid; + + if (action == Object::DELETE) { + dummy_object_info_pub_->publish(output_msg); + return; + } + + const auto object_tf = objects_.transform(uuid); + const auto object_twist = objects_.twist(uuid); + + if (!object_tf || !object_twist) { + return; + } + + // semantic + output_msg.classification.label = ObjectClassification::PEDESTRIAN; + output_msg.classification.probability = 1.0; + + // shape + output_msg.shape.type = Shape::CYLINDER; + const double width = 0.8; + const double length = 0.8; + output_msg.shape.dimensions.x = length; + output_msg.shape.dimensions.y = width; + output_msg.shape.dimensions.z = 2.0; + + // initial state + // pose + tf2::toMsg(object_tf.get(), output_msg.initial_state.pose_covariance.pose); + output_msg.initial_state.pose_covariance.pose.position.z = position_z_->getFloat(); + output_msg.initial_state.pose_covariance.covariance[0] = + std_dev_x_->getFloat() * std_dev_x_->getFloat(); + output_msg.initial_state.pose_covariance.covariance[7] = + std_dev_y_->getFloat() * std_dev_y_->getFloat(); + output_msg.initial_state.pose_covariance.covariance[14] = + std_dev_z_->getFloat() * std_dev_z_->getFloat(); + output_msg.initial_state.pose_covariance.covariance[35] = + std_dev_theta_->getFloat() * std_dev_theta_->getFloat(); + // twist + output_msg.initial_state.twist_covariance.twist = object_twist.get(); + + dummy_object_info_pub_->publish(output_msg); +} + +int PedestrianInitialPoseTool::processMouseEvent(rviz_common::ViewportMouseEvent & event) +{ + if (!enable_interactive_property_->getBool()) { + return PoseTool::processMouseEvent(event); + } + + if (event.rightDown()) { + const auto point = get_point_from_mouse(event); + if (point) { + if (event.shift()) { + const auto uuid = objects_.create(point.value()); + publishObjectMsg(uuid.get(), Object::ADD); + } else if (event.alt()) { + const auto uuid = objects_.remove(point.value()); + publishObjectMsg(uuid.get(), Object::DELETE); + } else { + objects_.select(point.value()); + } + } + return 0; + } + + if (event.rightUp()) { + objects_.reset(); + return 0; + } + + if (event.right()) { + const auto point = get_point_from_mouse(event); + if (point) { + const auto uuid = objects_.update(point.value()); + publishObjectMsg(uuid.get(), Object::MODIFY); + } + return 0; + } + + return move_tool_.processMouseEvent(event); +} + +int PedestrianInitialPoseTool::processKeyEvent(QKeyEvent * event, rviz_common::RenderPanel * panel) +{ + PoseTool::processKeyEvent(event, panel); + return move_tool_.processKeyEvent(event, panel); +} } // end namespace rviz_plugins #include diff --git a/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.hpp b/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.hpp index c463dbceeefd1..f7375eb734297 100644 --- a/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.hpp +++ b/common/tier4_perception_rviz_plugin/src/tools/pedestrian_pose.hpp @@ -51,15 +51,71 @@ #ifndef Q_MOC_RUN // See: https://bugreports.qt-project.org/browse/QTBUG-22829 #include #include +#include #include #include +#include +#include +#include #include #endif #include +#include + +#include +#include + +#include +#include +#include + namespace rviz_plugins { + +using autoware_auto_perception_msgs::msg::ObjectClassification; +using autoware_auto_perception_msgs::msg::Shape; +using dummy_perception_publisher::msg::Object; + +class InteractivePedestrian +{ +public: + explicit InteractivePedestrian(const Ogre::Vector3 & point); + ~InteractivePedestrian() {} + + std::array uuid() const; + void twist(geometry_msgs::msg::Twist & twist) const; + void transform(tf2::Transform & tf_map2object) const; + void update(const Ogre::Vector3 & point); + double distance(const Ogre::Vector3 & point); + +private: + std::array uuid_; + Ogre::Vector3 point_; + Ogre::Vector3 velocity_; + double theta_; +}; + +class InteractivePedestrianCollection +{ +public: + InteractivePedestrianCollection(); + ~InteractivePedestrianCollection() {} + + void reset(); + void select(const Ogre::Vector3 & point); + boost::optional> create(const Ogre::Vector3 & point); + boost::optional> remove(const Ogre::Vector3 & point); + boost::optional> update(const Ogre::Vector3 & point); + boost::optional twist(const std::array & uuid) const; + boost::optional transform(const std::array & uuid) const; + +private: + size_t nearest(const Ogre::Vector3 & point); + InteractivePedestrian * target_; + std::vector> objects_; +}; class PedestrianInitialPoseTool : public rviz_default_plugins::tools::PoseTool { Q_OBJECT @@ -68,6 +124,8 @@ class PedestrianInitialPoseTool : public rviz_default_plugins::tools::PoseTool PedestrianInitialPoseTool(); virtual ~PedestrianInitialPoseTool() {} virtual void onInitialize(); + int processMouseEvent(rviz_common::ViewportMouseEvent & event) override; + int processKeyEvent(QKeyEvent * event, rviz_common::RenderPanel * panel) override; protected: virtual void onPoseSet(double x, double y, double theta); @@ -79,6 +137,9 @@ private Q_SLOTS: rclcpp::Clock::SharedPtr clock_; rclcpp::Publisher::SharedPtr dummy_object_info_pub_; + rviz_default_plugins::tools::MoveTool move_tool_; + + rviz_common::properties::BoolProperty * enable_interactive_property_; rviz_common::properties::StringProperty * topic_property_; rviz_common::properties::FloatProperty * std_dev_x_; rviz_common::properties::FloatProperty * std_dev_y_; @@ -86,6 +147,11 @@ private Q_SLOTS: rviz_common::properties::FloatProperty * std_dev_theta_; rviz_common::properties::FloatProperty * position_z_; rviz_common::properties::FloatProperty * velocity_; + rviz_common::properties::TfFrameProperty * property_frame_; + + InteractivePedestrianCollection objects_; + + void publishObjectMsg(const std::array & uuid, const uint32_t action); }; } // namespace rviz_plugins diff --git a/common/tier4_perception_rviz_plugin/src/tools/util.cpp b/common/tier4_perception_rviz_plugin/src/tools/util.cpp new file mode 100644 index 0000000000000..50b68c2e85fd8 --- /dev/null +++ b/common/tier4_perception_rviz_plugin/src/tools/util.cpp @@ -0,0 +1,37 @@ +// Copyright 2022 TIER IV, Inc. +// +// 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 "util.hpp" + +#include +#include + +#include +#include +#include + +std::optional get_point_from_mouse(rviz_common::ViewportMouseEvent & event) +{ + using rviz_rendering::RenderWindowOgreAdapter; + const auto viewport = RenderWindowOgreAdapter::getOgreViewport(event.panel->getRenderWindow()); + const auto w = viewport->getActualWidth(); + const auto h = viewport->getActualHeight(); + const auto x = static_cast(event.x) / static_cast(w); + const auto y = static_cast(event.y) / static_cast(h); + + const auto plane = Ogre::Plane(Ogre::Vector3::UNIT_Z, 0.0); + const auto ray = viewport->getCamera()->getCameraToViewportRay(x, y); + const auto intersect = ray.intersects(plane); + return intersect.first ? std::optional(ray.getPoint(intersect.second)) : std::nullopt; +} diff --git a/common/tier4_perception_rviz_plugin/src/tools/util.hpp b/common/tier4_perception_rviz_plugin/src/tools/util.hpp new file mode 100644 index 0000000000000..2126af001aeb5 --- /dev/null +++ b/common/tier4_perception_rviz_plugin/src/tools/util.hpp @@ -0,0 +1,26 @@ +// Copyright 2022 TIER IV, Inc. +// +// 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 TOOLS__UTIL_HPP_ +#define TOOLS__UTIL_HPP_ + +#include + +#include + +#include + +std::optional get_point_from_mouse(rviz_common::ViewportMouseEvent & event); + +#endif // TOOLS__UTIL_HPP_ From 5aed14704f14e4f7492108d368a7687c6eb2e906 Mon Sep 17 00:00:00 2001 From: Tomoya Kimura Date: Fri, 18 Mar 2022 10:02:50 +0900 Subject: [PATCH 07/16] fix(multi_object_tracker): data association parameter (#541) * sort matrix Signed-off-by: tomoya.kimura * ANIMAL->TRAILER Signed-off-by: tomoya.kimura * apply change to another file Signed-off-by: tomoya.kimura * ci(pre-commit): autofix Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../data_association_matrix.param.yaml | 110 +++++++++--------- .../config/data_association_matrix.param.yaml | 110 +++++++++--------- 2 files changed, 112 insertions(+), 108 deletions(-) diff --git a/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml b/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml index 385eb335c3879..6e7ac3e9efe81 100644 --- a/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml +++ b/launch/tier4_perception_launch/config/object_recognition/tracking/multi_object_tracker/data_association_matrix.param.yaml @@ -1,62 +1,64 @@ /**: ros__parameters: can_assign_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [0, 0, 0, 0, 0, 0, 0, 0, #UNKNOWN - 1, 1, 1, 1, 0, 0, 0, 0, #CAR - 1, 1, 1, 1, 0, 0, 0, 0, #TRUCK - 1, 1, 1, 1, 0, 0, 0, 0, #BUS - 0, 0, 0, 0, 1, 1, 1, 0, #BICYCLE - 0, 0, 0, 0, 1, 1, 1, 0, #MOTORBIKE - 0, 0, 0, 0, 1, 1, 1, 0, #PEDESTRIAN - 0, 0, 0, 0, 0, 0, 0, 1] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE PEDESTRIAN + [0, 0, 0, 0, 0, 0, 0, 0, #UNKNOWN + 1, 1, 1, 1, 1, 0, 0, 0, #CAR + 1, 1, 1, 1, 1, 0, 0, 0, #TRUCK + 1, 1, 1, 1, 1, 0, 0, 0, #BUS + 1, 1, 1, 1, 1, 0, 0, 0, #TRAILER + 0, 0, 0, 0, 0, 1, 1, 1, #MOTORBIKE + 0, 0, 0, 0, 0, 1, 1, 1, #BICYCLE + 0, 0, 0, 0, 0, 1, 1, 1] #PEDESTRIAN + max_dist_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, #UNKNOWN - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #CAR - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #TRUCK - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #BUS - 3.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #BICYCLE - 3.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #MOTORBIKE - 2.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #PEDESTRIAN - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, #UNKNOWN + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #CAR + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #TRUCK + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #BUS + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #TRAILER + 3.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, #MOTORBIKE + 3.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, #BICYCLE + 2.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0] #PEDESTRIAN max_area_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, #UNKNOWN - 12.10, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #CAR - 19.75, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #TRUCK - 32.40, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #BUS - 2.50, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, 10000.00, #BICYCLE - 3.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, 10000.00, #MOTORBIKE - 2.00, 10000.00, 10000.00, 10000.00, 1.50, 1.50, 1.00, 10000.00, #PEDESTRIAN - 2.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, #UNKNOWN + 12.10, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #CAR + 19.75, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #TRUCK + 32.40, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #BUS + 32.40, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #TRAILER + 3.00, 10000.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, #MOTORBIKE + 2.50, 10000.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, #BICYCLE + 2.00, 10000.00, 10000.00, 10000.00, 10000.00, 1.50, 1.50, 1.00] #PEDESTRIAN min_area_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, #UNKNOWN - 3.600, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #CAR - 6.000, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #TRUCK - 10.000, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #BUS - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #BICYCLE - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #MOTORBIKE - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #PEDESTRIAN - 0.500, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, #UNKNOWN + 3.600, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #CAR + 6.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #TRUCK + 10.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #BUS + 10.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #TRAILER + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, #MOTORBIKE + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, #BICYCLE + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100] #PEDESTRIAN max_rad_matrix: # If value is greater than pi, it will be ignored. - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #UNKNOWN - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #CAR - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #TRUCK - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #BUS - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #BICYCLE - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #PEDESTRIAN - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER MOTORBIKE, BICYCLE, PEDESTRIAN + [3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #UNKNOWN + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #CAR + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #TRUCK + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #BUS + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #TRAILER + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #BICYCLE + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #PEDESTRIAN + min_iou_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN - 0.1, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, #CAR - 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #TRUCK - 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #BUS - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #PEDESTRIAN - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN + 0.1, 0.3, 0.2, 0.2, 0.2, 0.1, 0.1, 0.1, #CAR + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #TRUCK + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #BUS + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #TRAILER + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #PEDESTRIAN diff --git a/perception/multi_object_tracker/config/data_association_matrix.param.yaml b/perception/multi_object_tracker/config/data_association_matrix.param.yaml index 385eb335c3879..6e7ac3e9efe81 100644 --- a/perception/multi_object_tracker/config/data_association_matrix.param.yaml +++ b/perception/multi_object_tracker/config/data_association_matrix.param.yaml @@ -1,62 +1,64 @@ /**: ros__parameters: can_assign_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [0, 0, 0, 0, 0, 0, 0, 0, #UNKNOWN - 1, 1, 1, 1, 0, 0, 0, 0, #CAR - 1, 1, 1, 1, 0, 0, 0, 0, #TRUCK - 1, 1, 1, 1, 0, 0, 0, 0, #BUS - 0, 0, 0, 0, 1, 1, 1, 0, #BICYCLE - 0, 0, 0, 0, 1, 1, 1, 0, #MOTORBIKE - 0, 0, 0, 0, 1, 1, 1, 0, #PEDESTRIAN - 0, 0, 0, 0, 0, 0, 0, 1] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE PEDESTRIAN + [0, 0, 0, 0, 0, 0, 0, 0, #UNKNOWN + 1, 1, 1, 1, 1, 0, 0, 0, #CAR + 1, 1, 1, 1, 1, 0, 0, 0, #TRUCK + 1, 1, 1, 1, 1, 0, 0, 0, #BUS + 1, 1, 1, 1, 1, 0, 0, 0, #TRAILER + 0, 0, 0, 0, 0, 1, 1, 1, #MOTORBIKE + 0, 0, 0, 0, 0, 1, 1, 1, #BICYCLE + 0, 0, 0, 0, 0, 1, 1, 1] #PEDESTRIAN + max_dist_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, #UNKNOWN - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #CAR - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #TRUCK - 4.0, 2.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, #BUS - 3.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #BICYCLE - 3.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #MOTORBIKE - 2.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, 1.0, #PEDESTRIAN - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, #UNKNOWN + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #CAR + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #TRUCK + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #BUS + 4.0, 2.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, #TRAILER + 3.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, #MOTORBIKE + 3.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0, #BICYCLE + 2.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 2.0] #PEDESTRIAN max_area_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, #UNKNOWN - 12.10, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #CAR - 19.75, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #TRUCK - 32.40, 12.10, 19.75, 32.40, 10000.00, 10000.00, 10000.00, 10000.00, #BUS - 2.50, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, 10000.00, #BICYCLE - 3.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, 10000.00, #MOTORBIKE - 2.00, 10000.00, 10000.00, 10000.00, 1.50, 1.50, 1.00, 10000.00, #PEDESTRIAN - 2.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, 10000.00, #UNKNOWN + 12.10, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #CAR + 19.75, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #TRUCK + 32.40, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #BUS + 32.40, 12.10, 19.75, 32.40, 32.40, 10000.00, 10000.00, 10000.00, #TRAILER + 3.00, 10000.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, #MOTORBIKE + 2.50, 10000.00, 10000.00, 10000.00, 10000.00, 2.50, 2.50, 1.00, #BICYCLE + 2.00, 10000.00, 10000.00, 10000.00, 10000.00, 1.50, 1.50, 1.00] #PEDESTRIAN min_area_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, #UNKNOWN - 3.600, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #CAR - 6.000, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #TRUCK - 10.000, 3.600, 6.000, 10.000, 0.000, 0.000, 0.000, 0.000, #BUS - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #BICYCLE - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #MOTORBIKE - 0.001, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, 0.000, #PEDESTRIAN - 0.500, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, #UNKNOWN + 3.600, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #CAR + 6.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #TRUCK + 10.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #BUS + 10.000, 3.600, 6.000, 10.000, 10.000, 0.000, 0.000, 0.000, #TRAILER + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, #MOTORBIKE + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100, #BICYCLE + 0.001, 0.000, 0.000, 0.000, 0.000, 0.100, 0.100, 0.100] #PEDESTRIAN max_rad_matrix: # If value is greater than pi, it will be ignored. - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #UNKNOWN - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #CAR - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #TRUCK - 3.150, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, 3.150, #BUS - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #BICYCLE - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #PEDESTRIAN - 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER MOTORBIKE, BICYCLE, PEDESTRIAN + [3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #UNKNOWN + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #CAR + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #TRUCK + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #BUS + 3.150, 1.047, 1.047, 1.047, 1.047, 3.150, 3.150, 3.150, #TRAILER + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #MOTORBIKE + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, #BICYCLE + 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150] #PEDESTRIAN + min_iou_matrix: - #UNKNOWN, CAR, TRUCK, BUS, BICYCLE, MOTORBIKE, PEDESTRIAN, ANIMAL - [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN - 0.1, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, #CAR - 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #TRUCK - 0.1, 0.2, 0.3, 0.3, 0.1, 0.1, 0.1, 0.1, #BUS - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #PEDESTRIAN - 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #ANIMAL + #UNKNOWN, CAR, TRUCK, BUS, TRAILER, MOTORBIKE, BICYCLE, PEDESTRIAN + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #UNKNOWN + 0.1, 0.3, 0.2, 0.2, 0.2, 0.1, 0.1, 0.1, #CAR + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #TRUCK + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #BUS + 0.1, 0.2, 0.3, 0.3, 0.3, 0.1, 0.1, 0.1, #TRAILER + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #MOTORBIKE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, #BICYCLE + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] #PEDESTRIAN From 4536e52bdbc80ea3dec0d2940ded51ffd118b477 Mon Sep 17 00:00:00 2001 From: Takayuki Murooka Date: Fri, 18 Mar 2022 10:17:44 +0900 Subject: [PATCH 08/16] feat(obstacle avoidance planner): fix out curve, make calculation cost low, make optimization stable, refactor, etc. (#233) * feat (obstacle avoidance planner) fix out curve, make optimization stable, make computation cost low, etc Signed-off-by: Takayuki Murooka * fix typos Signed-off-by: Takayuki Murooka * remove unnecessary codes Signed-off-by: Takayuki Murooka * minor refactoring Signed-off-by: Takayuki Murooka * add path shape change detection for replan Signed-off-by: Takayuki Murooka * add feature explanation in README Signed-off-by: Takayuki Murooka * move some parameters to advanced Signed-off-by: Takayuki Murooka * calcualte radius num automatically Signed-off-by: Takayuki Murooka * update config in tier4_planning_launch Signed-off-by: Takayuki Murooka * truncate path to detect path change, and tune path change detection Signed-off-by: Takayuki Murooka * disable yaw slerp Signed-off-by: Takayuki Murooka * fix ci error Signed-off-by: Takayuki Murooka --- .../obstacle_avoidance_planner.param.yaml | 251 +- .../obstacle_avoidance_planner/CMakeLists.txt | 8 +- planning/obstacle_avoidance_planner/README.md | 660 ++++- .../obstacle_avoidance_planner.param.yaml | 260 +- .../common_structs.hpp | 255 ++ .../costmap_generator.hpp | 66 + .../obstacle_avoidance_planner/cv_utils.hpp | 121 + .../debug_visualization.hpp | 47 + .../eb_path_optimizer.hpp | 447 +-- .../mpt_optimizer.hpp | 375 +-- .../obstacle_avoidance_planner/node.hpp | 283 +- .../obstacle_avoidance_planner/util.hpp | 203 -- .../obstacle_avoidance_planner/utils.hpp | 336 +++ .../vehicle_model_bicycle_kinematics.hpp | 35 +- .../vehicle_model/vehicle_model_interface.hpp | 50 +- .../obstacle_avoidance_planner.launch.xml | 2 - .../media/behavior_path.png | Bin 0 -> 14745 bytes .../media/bounds.png | Bin 0 -> 18960 bytes .../media/drivable_area.png | Bin 62331 -> 14299 bytes .../media/eb_traj.png | Bin 0 -> 15264 bytes .../media/mpt_fixed_traj.png | Bin 0 -> 13916 bytes .../media/mpt_ref_traj.png | Bin 0 -> 14982 bytes .../media/mpt_traj.png | Bin 0 -> 16265 bytes .../media/output_traj.png | Bin 0 -> 17862 bytes .../obstacle_avoidance_planner-design.ja.md | 330 +++ .../obstacle_avoidance_planner/package.xml | 1 + .../src/costmap_generator.cpp | 342 +++ .../src/cv_utils.cpp | 463 ++++ .../obstacle_avoidance_planner/src/debug.cpp | 1 - .../src/debug_visualization.cpp | 826 ++++++ .../src/eb_path_optimizer.cpp | 1396 ++-------- .../src/mpt_optimizer.cpp | 2427 ++++++++++------- .../obstacle_avoidance_planner/src/node.cpp | 1912 ++++++++----- .../src/process_cv.cpp | 1 - .../obstacle_avoidance_planner/src/util.cpp | 1018 ------- .../obstacle_avoidance_planner/src/utils.cpp | 551 ++++ .../vehicle_model_bicycle_kinematics.cpp | 63 +- .../vehicle_model/vehicle_model_interface.cpp | 17 +- 38 files changed, 7632 insertions(+), 5115 deletions(-) create mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/common_structs.hpp create mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/costmap_generator.hpp create mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/cv_utils.hpp create mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp delete mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/util.hpp create mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/utils.hpp create mode 100644 planning/obstacle_avoidance_planner/media/behavior_path.png create mode 100644 planning/obstacle_avoidance_planner/media/bounds.png create mode 100644 planning/obstacle_avoidance_planner/media/eb_traj.png create mode 100644 planning/obstacle_avoidance_planner/media/mpt_fixed_traj.png create mode 100644 planning/obstacle_avoidance_planner/media/mpt_ref_traj.png create mode 100644 planning/obstacle_avoidance_planner/media/mpt_traj.png create mode 100644 planning/obstacle_avoidance_planner/media/output_traj.png create mode 100644 planning/obstacle_avoidance_planner/obstacle_avoidance_planner-design.ja.md create mode 100644 planning/obstacle_avoidance_planner/src/costmap_generator.cpp create mode 100644 planning/obstacle_avoidance_planner/src/cv_utils.cpp create mode 100644 planning/obstacle_avoidance_planner/src/debug_visualization.cpp delete mode 100644 planning/obstacle_avoidance_planner/src/util.cpp create mode 100644 planning/obstacle_avoidance_planner/src/utils.cpp diff --git a/launch/tier4_planning_launch/config/scenario_planning/lane_driving/motion_planning/obstacle_avoidance_planner/obstacle_avoidance_planner.param.yaml b/launch/tier4_planning_launch/config/scenario_planning/lane_driving/motion_planning/obstacle_avoidance_planner/obstacle_avoidance_planner.param.yaml index 8150af7436212..40b64b7d87012 100644 --- a/launch/tier4_planning_launch/config/scenario_planning/lane_driving/motion_planning/obstacle_avoidance_planner/obstacle_avoidance_planner.param.yaml +++ b/launch/tier4_planning_launch/config/scenario_planning/lane_driving/motion_planning/obstacle_avoidance_planner/obstacle_avoidance_planner.param.yaml @@ -1,90 +1,173 @@ /**: ros__parameters: - # trajectory total/fixing length - trajectory_length: 300 # total trajectory length[m] - forward_fixing_distance: 5.0 # forward fixing length from base_link[m] - backward_fixing_distance: 3.0 # backward fixing length from base_link[m] - - # clearance(distance) when generating trajectory - clearance_from_road: 0.2 # clearance from road boundary[m] - clearance_from_object: 1.0 # clearance from object[m] - min_object_clearance_for_joint: 3.2 # minimum clearance from object[m] when judging is_including_only_smooth_range - extra_desired_clearance_from_road: 0.0 # extra desired clearance from road - - # clearance for unique points - clearance_for_straight_line: 0.05 # minimum optimizing range around straight points - clearance_for_joint: 0.1 # minimum optimizing range around joint points - clearance_for_only_smoothing: 0.1 # minimum optimizing range when applying only smoothing - range_for_extend_joint: 0.1 # minimum optimizing range around joint points for extending - clearance_from_object_for_straight: 10.0 # minimum clearance from object when judging straight line - - # avoiding param - max_avoiding_objects_velocity_ms: 0.5 # maximum velocity for avoiding objects[m/s] - max_avoiding_ego_velocity_ms: 6.0 # maximum ego velocity when avoiding objects[m/s] - center_line_width: 1.7 # replaced with /vehicle_info/vehicle_width when is_using_vehicle_config is true, center line width around path points used for judging that object is required to avoid or not - acceleration_for_non_deceleration_range: 1.0 # assumed acceleration for calculating non deceleration range - - # solving quadratic programming - qp_max_iteration: 10000 # max iteration when solving QP - qp_eps_abs: 1.0e-8 # eps abs when solving OSQP - qp_eps_rel: 1.0e-11 # eps rel when solving OSQP - qp_eps_abs_for_extending: 1.0e-8 # eps abs when solving OSQP for extending - qp_eps_rel_for_extending: 1.0e-9 # eps rel when solving OSQP for extending - qp_eps_abs_for_visualizing: 1.0e-6 # eps abs when solving OSQP for visualizing - qp_eps_rel_for_visualizing: 1.0e-8 # eps rel when solving OSQP for visualizing - - # constrain space - is_getting_constraints_close2path_points: true # generate trajectory closer to path points - max_x_constrain_search_range: 0.0 # maximum x search range in local coordinate - coef_x_constrain_search_resolution: 1.0 # coef for fine sampling when exploring x direction - coef_y_constrain_search_resolution: 0.5 # coef for fine sampling when exploring y direction - keep_space_shape_x: 0.2 # keep space for x direction from base_link[m] - keep_space_shape_y: 0.1 # replaced with /vehicle_info/vehicle_width when is_using_vehicle_config is true, keep space for y direction from base_link[m] - max_lon_space_for_driveable_constraint: 0.5 # maximum lon(x) space when optimizing point[m] - - is_publishing_clearance_map: true # publish clearance map as nav_msgs::OccupancyGrid - is_publishing_area_with_objects: true # publish occupancy map as nav_msgs::OccupancyGrid - enable_avoidance: true # enable avoidance function - is_using_vehicle_config: true # use vehicle config - num_sampling_points: 100 # number of optimizing points - num_joint_buffer_points: 3 # number of joint buffer points - num_joint_buffer_points_for_extending: 6 # number of joint buffer points for extending - num_offset_for_begin_idx: 2 # number of shifting points for beginning non-fix idx - num_fix_points_for_extending: 50 # number of fixing points when extending - delta_arc_length_for_optimization: 1.0 # delta arc length when optimizing[m] - delta_arc_length_for_trajectory: 0.1 # delta arc length for trajectory[m] - delta_dist_threshold_for_closest_point: 3.0 # delta dist thres for closest point[m] - delta_yaw_threshold_for_closest_point: 1.046 #M_PI/3.0, delta yaw thres for closest point - delta_yaw_threshold_for_straight: 0.02 # delta dist thres for straight point + option: + # publish + is_publishing_debug_visualization_marker: true + is_publishing_clearance_map: false # publish clearance map as nav_msgs::OccupancyGrid + is_publishing_object_clearance_map: false # publish clearance map as nav_msgs::OccupancyGrid + is_publishing_area_with_objects: false # publish occupancy map as nav_msgs::OccupancyGrid + + is_stopping_if_outside_drivable_area: true # stop if the ego's footprint will be outside the drivable area + + # show + is_showing_debug_info: false + is_showing_calculation_time: false + + # other + enable_avoidance: false # enable avoidance function + enable_pre_smoothing: true # enable EB + skip_optimization: false # skip MPT and EB + reset_prev_optimization: false + + common: + # sampling + num_sampling_points: 100 # number of optimizing points + + # trajectory total/fixing length + trajectory_length: 300.0 # total trajectory length[m] + + forward_fixing_min_distance: 1.0 # number of fixing points around ego vehicle [m] + forward_fixing_min_time: 0.5 # forward fixing time with current velocity [s] + + backward_fixing_distance: 5.0 # backward fixing length from base_link [m] + delta_arc_length_for_trajectory: 0.1 # delta arc length for trajectory [m] + + delta_dist_threshold_for_closest_point: 3.0 # delta dist thres for closest point [m] + delta_yaw_threshold_for_closest_point: 1.046 #M_PI/3.0, delta yaw thres for closest point + delta_yaw_threshold_for_straight: 0.02 # delta dist thres for straight point + + num_fix_points_for_extending: 50 # number of fixing points when extending + max_dist_for_extending_end_point: 0.0001 # minimum delta dist thres for extending last point [m] + + object: # avoiding object + max_avoiding_objects_velocity_ms: 0.5 # maximum velocity for avoiding objects [m/s] + max_avoiding_ego_velocity_ms: 6.0 # maximum ego velocity when avoiding objects [m/s] + + avoiding_object_type: + unknown: true + car: true + truck: true + bus: true + bicycle: true + motorbike: true + pedestrian: true + animal: true # mpt param - # vehicle param for mpt - max_steer_deg: 40.0 # max steering angle [deg] - steer_tau: 0.1 # steering dynamics time constant (1d approximation) [s] - - # trajectory param for mpt - num_curvature_sampling_points: 5 # number of sampling points when calculating curvature - delta_arc_length_for_mpt_points: 0.5 # delta arc length when generating mpt[m] - forward_fixing_mpt_distance: 10 # number of fixing points around ego vehicle - - # optimization param for mpt - is_hard_fixing_terminal_point: false # hard fixing terminal point - base_point_weight: 2000.0 # slack weight for lateral error around base_link - top_point_weight: 1000.0 # slack weight for lateral error around vehicle-top point - mid_point_weight: 1000.0 # slack weight for lateral error around the middle point - lat_error_weight: 10.0 # weight for lateral error - yaw_error_weight: 0.0 # weight for yaw error - steer_input_weight: 0.01 # weight for steering input - steer_rate_weight: 1.0 # weight for steering rate - steer_acc_weight: 0.000001 # weight for steering acceleration - terminal_lat_error_weight: 0.0 # weight for lateral error at terminal point - terminal_yaw_error_weight: 100.0 # weight for yaw error at terminal point - terminal_path_lat_error_weight: 1000.0 # weight for lateral error at path end point - terminal_path_yaw_error_weight: 1000.0 # weight for yaw error at path end point - zero_ff_steer_angle: 0.5 # threshold that feed-forward angle becomes zero + mpt: + option: + steer_limit_constraint: true + fix_points_around_ego: true + # plan_from_ego: false + visualize_sampling_num: 1 + enable_manual_warm_start: true + enable_warm_start: true # false + + common: + num_curvature_sampling_points: 5 # number of sampling points when calculating curvature + delta_arc_length_for_mpt_points: 0.5 # delta arc length when generating mpt [m] + + kinematics: + max_steer_deg: 40.0 # max steering angle [deg] + + # If this parameter is commented out, the parameter is set as below by default. + # The logic could be `optimization_center_offset = vehicle_info.wheel_base * 0.8` + # The 0.8 scale is adopted as it performed the best. + # optimization_center_offset: 2.3 # optimization center offset from base link # replanning & trimming trajectory param outside algorithm - min_delta_dist_for_replan: 10.0 # minimum delta dist thres for replan[m] - min_delta_time_sec_for_replan: 1.0 # minimum delta time for replan[second] - max_dist_for_extending_end_point: 5.0 # minimum delta dist thres for extending last point[m] - distance_for_path_shape_change_detection: 2.0 # minimum delta dist thres for detecting path shape change + replan: + max_path_shape_change_dist: 0.3 # threshold of path shape change from behavior path [m] + max_ego_moving_dist_for_replan: 3.0 # threshold of ego's moving distance for replan [m] + max_delta_time_sec_for_replan: 1.0 # threshold of delta time for replan [second] + + # advanced parameters to improve performance as much as possible + advanced: + option: + # check if planned trajectory is outside drivable area + drivability_check: + # true: vehicle shape is considered as a set of circles + # false: vehicle shape is considered as footprint (= rectangle) + use_vehicle_circles: false + + # parameters only when use_vehicle_circles is true + vehicle_circles: + use_manual_vehicle_circles: false + num_for_constraints: 4 + + # parameters only when use_manual_vehicle_circles is true + longitudinal_offsets: [-0.15, 0.88, 1.9, 2.95] + radius: 0.95 + + # parameters only when use_manual_vehicle_circles is false + radius_ratio: 0.9 + # If this parameter is commented out, the parameter is calculated automatically + # based on the vehicle length and width + # num_for_radius: 4 + + eb: + common: + num_joint_buffer_points: 3 # number of joint buffer points + num_offset_for_begin_idx: 2 # number of shifting points for beginning non-fix idx + delta_arc_length_for_eb: 0.6 # 1.0 # delta arc length when optimizing[m] When the value is around 1.0, overshoot at the corner happens. + num_sampling_points_for_eb: 95 # number of optimizing points # must be longer than mpt points + + clearance: + clearance_for_straight_line: 0.05 # minimum optimizing range around straight points + clearance_for_joint: 0.1 # minimum optimizing range around joint points + clearance_for_only_smoothing: 0.1 # minimum optimizing range when applying only smoothing + + qp: + max_iteration: 10000 # max iteration when solving QP + eps_abs: 1.0e-8 # eps abs when solving OSQP + eps_rel: 1.0e-10 # eps rel when solving OSQP + + mpt: + clearance: # clearance(distance) between vehicle and roads/objects when generating trajectory + hard_clearance_from_road: 0.0 # clearance from road boundary[m] + soft_clearance_from_road: 0.1 # clearance from road boundary[m] + soft_second_clearance_from_road: 1.0 # clearance from road boundary[m] + clearance_from_object: 1.0 # clearance from object[m] + extra_desired_clearance_from_road: 0.0 # extra desired clearance from road + + weight: + soft_avoidance_weight: 1000.0 # slack weight for lateral error around the middle point + soft_second_avoidance_weight: 100.0 # slack weight for lateral error around the middle point + + lat_error_weight: 100.0 # weight for lateral error + yaw_error_weight: 0.0 # weight for yaw error + yaw_error_rate_weight: 0.0 # weight for yaw error rate + steer_input_weight: 10.0 # weight for steering input + steer_rate_weight: 10.0 # weight for steering rate + + obstacle_avoid_lat_error_weight: 3.0 # weight for lateral error + obstacle_avoid_yaw_error_weight: 0.0 # weight for yaw error + obstacle_avoid_steer_input_weight: 1000.0 # weight for yaw error + near_objects_length: 30.0 # weight for yaw error + + terminal_lat_error_weight: 100.0 # weight for lateral error at terminal point + terminal_yaw_error_weight: 100.0 # weight for yaw error at terminal point + terminal_path_lat_error_weight: 1000.0 # weight for lateral error at path end point + terminal_path_yaw_error_weight: 1000.0 # weight for yaw error at path end point + + # check if planned trajectory is outside drivable area + collision_free_constraints: + option: + l_inf_norm: true + soft_constraint: true + hard_constraint: false + # two_step_soft_constraint: false + + vehicle_circles: + use_manual_vehicle_circles: false + num_for_constraints: 3 + + # parameters only when use_manual_vehicle_circles is true + longitudinal_offsets: [-0.15, 0.88, 1.9, 2.95] + radius: 0.95 + + # parameters only when use_manual_vehicle_circles is false + radius_ratio: 0.8 + # If this parameter is commented out, the parameter is calculated automatically + # based on the vehicle length and width + # num_for_radius: 4 diff --git a/planning/obstacle_avoidance_planner/CMakeLists.txt b/planning/obstacle_avoidance_planner/CMakeLists.txt index 0c42f35d44206..bc694f79f46a1 100644 --- a/planning/obstacle_avoidance_planner/CMakeLists.txt +++ b/planning/obstacle_avoidance_planner/CMakeLists.txt @@ -19,12 +19,12 @@ find_package(OpenCV REQUIRED) ament_auto_add_library(obstacle_avoidance_planner SHARED src/vehicle_model/vehicle_model_interface.cpp src/vehicle_model/vehicle_model_bicycle_kinematics.cpp - src/vehicle_model/vehicle_model_bicycle_kinematics_no_delay.cpp - src/util.cpp - src/debug.cpp + src/utils.cpp + src/costmap_generator.cpp + src/debug_visualization.cpp src/eb_path_optimizer.cpp src/mpt_optimizer.cpp - src/process_cv.cpp + src/cv_utils.cpp src/node.cpp ) diff --git a/planning/obstacle_avoidance_planner/README.md b/planning/obstacle_avoidance_planner/README.md index f97e50aeea414..f2b3587274b06 100644 --- a/planning/obstacle_avoidance_planner/README.md +++ b/planning/obstacle_avoidance_planner/README.md @@ -1,9 +1,15 @@ -# Obstacle Avoidance Planner - ## Purpose This package generates a trajectory that is feasible to drive and collision free based on a reference path, drivable area, and static/dynamic obstacles. -Only position and orientation of trajectory are calculated in this module, and velocity or acceleration will be updated in the latter modules. +Only position and orientation of trajectory are calculated in this module (velocity is just aligned from the one in the path), and velocity or acceleration will be updated in the latter modules. + +## Feature + +This package is able to + +- follow the behavior path smoothly +- make the trajectory inside the drivable area as much as possible +- insert stop point if its trajectory point is outside the drivable area ## Inputs / Outputs @@ -13,34 +19,84 @@ Only position and orientation of trajectory are calculated in this module, and v | ---------------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------- | | `~/input/path` | autoware_auto_planning_msgs/Path | Reference path and the corresponding drivable area | | `~/input/objects` | autoware_auto_perception_msgs/PredictedObjects | Recognized objects around the vehicle | -| `/localization/kinematic_state` | nav_msgs/Odometry | Current Velocity of ego vehicle | +| `/localization/kinematic_kinematics` | nav_msgs/Odometry | Current Velocity of ego vehicle | | `/planning/scenario_planning/lane_driving/obstacle_avoidance_approval` | tier4_planning_msgs/EnableAvoidance | Approval to execute obstacle avoidance | ### output -| Name | Type | Description | -| --------------- | -------------------------------------- | ----------------------------------------------------------------- | -| `~/output/path` | autoware_auto_planning_msgs/Trajectory | Optimized trajectory that is feasible to drive and collision-free | +| Name | Type | Description | +| --------------------- | -------------------------------------- | ----------------------------------------------------------------- | +| `~/output/trajectory` | autoware_auto_planning_msgs/Trajectory | Optimized trajectory that is feasible to drive and collision-free | ## Flowchart -Each module is explained briefly here based on the flowchart. +Flowchart of functions is explained here. + +```plantuml +@startuml +title pathCallback +start + +group generateOptimizedTrajectory + :checkReplan; + if (replanning required?) then (yes) + group getMaps + :getDrivableArea; + :getRoadClearanceMap; + :drawObstacleOnImage; + :getObstacleClearanceMap; + end group + + group optimizeTrajectory + :getEBTrajectory; + :getModelPredictiveTrajectory; + + if (optimization failed?) then (no) + else (yes) + :send previous\n trajectory; + endif + end group + + :insertZeroVelocityOutsideDrivableArea; + + :publishDebugDataInOptimization; + else (no) + :send previous\n trajectory; + endif +end group + + +group generatePostProcessedTrajectory + :getExtendedOptimizedTrajectory; + :concatTrajectory; + :generateFineTrajectoryPoints; + :alignVelocity; +end group -![obstacle_avoidance_planner_flowchart](./media/obstacle_avoidance_planner_flowchart.drawio.svg) +:convertToTrajectory; -### Manage trajectory generation +:publishDebugDataInMain; -When one of the following conditions area realized, callback function to generate a trajectory is called and publish the trajectory. -Otherwise, previously generated trajectory is published. +stop +@enduml +``` -- Ego moves a certain distance compared to the previous ego pose (default: 10.0 [m]) +### checkReplan + +When one of the following conditions are realized, callback function to generate a trajectory is called and publish the trajectory. +Otherwise, previously generated (optimized) trajectory is published just with aligning the velocity from the latest behavior path. + +- Ego moves a certain distance compared to the previous ego pose (default: 3.0 [m]) - Time passes (default: 1.0 [s]) -- The path shape is changed (e.g. when starting planning lane change) - Ego is far from the previously generated trajectory -The previously generated trajectory is memorized, but it is not when the path shape is changed and ego is far from the previously generated trajectory. +### getRoadClearanceMap + +Cost map is generated according to the distance to the road boundaries. + +These cost maps are used in the optimization to generate a collision-free trajectory. -### Select objects to avoid +### drawObstacleOnImage Only obstacles that are static and locate in a shoulder lane is decided to avoid. In detail, this equals to the following three conditions at the same time, and the red obstacles in the figure (id: 3, 4, 5) is to be avoided. @@ -52,39 +108,37 @@ In detail, this equals to the following three conditions at the same time, and t ![obstacle_to_avoid](./media/obstacle_to_avoid.drawio.svg) -### Generate object costmap +### getObstacleClearanceMap Cost map is generated according to the distance to the target obstacles to be avoided. -### Generate road boundary costmap - -Cost map is generated according to the distance to the road boundaries. - -These cost maps area used in the optimization to generate a collision-free trajectory. +### getEBTrajectory -### Smooth path +The latter optimization (MPT) assumes that the reference path is smooth enough. +Therefore the path from behavior is made smooth here, and send to the optimization as a format of trajectory. +Obstacles are ignored in this function. -The latter optimization assumes that the reference path is smooth enough. -Therefore the path from behavior is smoothed here, and send to the optimization as a format of trajectory. -Obstacles are not considered. +More details can be seen in the Elastic Band section. -### Optimize trajectory +### getModelPredictiveTrajectory This module makes the trajectory kinematically-feasible and collision-free. We define vehicle pose in the frenet coordinate, and minimize tracking errors by optimization. -This optimization also considers vehicle kinematics and collision checking with road boundary and obstacles. -To decrease the computation cost, the optimization is applied to the shorter trajectory than the whole trajectory, and concatenate the remained trajectory with the optimized one at last. +This optimization considers vehicle kinematics and collision checking with road boundary and obstacles. +To decrease the computation cost, the optimization is applied to the shorter trajectory (default: 50 [m]) than the whole trajectory, and concatenate the remained trajectory with the optimized one at last. The trajectory just in front of the ego must not be changed a lot so that the steering wheel will be stable. Therefore, we use the previously generated trajectory in front of the ego. Optimization center on the vehicle, that tries to locate just on the trajectory, can be tuned along side the vehicle vertical axis. -This parameter `optimization center offset` is defined as the signed length from the back-wheel center to the optimization center. +This parameter `mpt.kinematics.optimization center offset` is defined as the signed length from the back-wheel center to the optimization center. Some examples are shown in the following figure, and it is shown that the trajectory of vehicle shape differs according to the optimization center even if the reference trajectory (green one) is the same. ![mpt_optimization_offset](./media/mpt_optimization_offset.svg) -### Check drivability, and extend trajectory +More details can be seen in the Model Predictive Trajectory section. + +### insertZeroVelocityOutsideDrivableArea Optimized trajectory is too short for velocity planning, therefore extend the trajectory by concatenating the optimized trajectory and the behavior path considering drivability. Generated trajectory is checked if it is inside the drivable area or not, and if outside drivable area, output a trajectory inside drivable area with the behavior path or the previously generated trajectory. @@ -96,9 +150,9 @@ As described above, the behavior path is separated into two paths: one is for op - In this case, we do not care if the remained trajectory is inside or outside the drivable area since generally it is outside the drivable area (especially in a narrow road), but we want to pass a trajectory as long as possible to the latter module. - If optimized trajectory is **outside the drivable area**, and the remained trajectory is inside/outside the drivable area, - and if the previously generated trajectory **is memorized**, - - the output trajectory will be a part of previously generated trajectory, whose end firstly goes outside the drivable area. + - the output trajectory will be the previously generated trajectory, where zero velocity is inserted to the point firstly going outside the drivable area. - and if the previously generated trajectory **is not memorized**, - - the output trajectory will be a part of trajectory just transformed from the behavior path, whose end firstly goes outside the drivable area. + - the output trajectory will be a part of trajectory just transformed from the behavior path, where zero velocity is inserted to the point firstly going outside the drivable area. Optimization failure is dealt with the same as if the optimized trajectory is outside the drivable area. The output trajectory is memorized as a previously generated trajectory for the next cycle. @@ -107,14 +161,14 @@ _Rationale_ In the current design, since there are some modelling errors, the constraints are considered to be soft constraints. Therefore, we have to make sure that the optimized trajectory is inside the drivable area or not after optimization. -### Assign trajectory velocity +### alignVelocity -Velocity is assigned in the result trajectory by the behavior path. +Velocity is assigned in the result trajectory from the velocity in the behavior path. The shapes of the trajectory and the path are different, therefore the each nearest trajectory point to the path is searched and interpolated linearly. ## Algorithms -In this section, Elastic band (to smooth the path) and Model Predictive Trajectory (to optimize the trajectory) will be explained in detail. +In this section, Elastic band (to make the path smooth) and Model Predictive Trajectory (to make the trajectory kinematically feasible and collision-free) will be explained in detail. ### Elastic band @@ -131,11 +185,68 @@ Therefore the output path may have a collision with road boundaries or obstacles We formulate a QP problem minimizing the distance between the previous point and the next point for each point. Conditions that each point can move to a certain extent are used so that the path will not changed a lot but will be smoother. -For $k$'th point ($\boldsymbol{p}_k$), the objective function is as follows. +For $k$'th point ($\boldsymbol{p}_k = (x_k, y_k)$), the objective function is as follows. The beginning and end point are fixed during the optimization. $$ -\min \sum_{k=1}^{n-1} |\boldsymbol{p}_{k+1} - \boldsymbol{p}_{k}| - |\boldsymbol{p}_{k} - \boldsymbol{p}_{k-1}| +\begin{align} +\ J & = \min \sum_{k=1}^{n-1} ||(\boldsymbol{p}_{k+1} - \boldsymbol{p}_{k}) - (\boldsymbol{p}_{k} - \boldsymbol{p}_{k-1})||^2 \\ +\ & = \min \sum_{k=1}^{n-1} ||\boldsymbol{p}_{k+1} - 2 \boldsymbol{p}_{k} + \boldsymbol{p}_{k-1}||^2 \\ +\ & = \min \sum_{k=1}^{n-1} \{(x_{k+1} - x_k + x_{k-1})^2 + (y_{k+1} - y_k + y_{k-1})^2\} \\ +\ & = \min + \begin{pmatrix} + \ x_0 \\ + \ x_1 \\ + \ x_2 \\ + \vdots \\ + \ x_{n-2}\\ + \ x_{n-1} \\ + \ x_{n} \\ + \ y_0 \\ + \ y_1 \\ + \ y_2 \\ + \vdots \\ + \ y_{n-2}\\ + \ y_{n-1} \\ + \ y_{n} \\ + \end{pmatrix}^T + \begin{pmatrix} + 1 & -2 & 1 & 0 & \dots& \\ + -2 & 5 & -4 & 1 & 0 &\dots \\ + 1 & -4 & 6 & -4 & 1 & \\ + 0 & 1 & -4 & 6 & -4 & \\ + \vdots & 0 & \ddots&\ddots& \ddots \\ + & \vdots & & & \\ + & & & 1 & -4 & 6 & -4 & 1 \\ + & & & & 1 & -4 & 5 & -2 \\ + & & & & & 1 & -2 & 1& \\ + & & & & & & & &1 & -2 & 1 & 0 & \dots& \\ + & & & & & & & &-2 & 5 & -4 & 1 & 0 &\dots \\ + & & & & & & & &1 & -4 & 6 & -4 & 1 & \\ + & & & & & & & &0 & 1 & -4 & 6 & -4 & \\ + & & & & & & & &\vdots & 0 & \ddots&\ddots& \ddots \\ + & & & & & & & & & \vdots & & & \\ + & & & & & & & & & & & 1 & -4 & 6 & -4 & 1 \\ + & & & & & & & & & & & & 1 & -4 & 5 & -2 \\ + & & & & & & & & & & & & & 1 & -2 & 1& \\ + \end{pmatrix} + \begin{pmatrix} + \ x_0 \\ + \ x_1 \\ + \ x_2 \\ + \vdots \\ + \ x_{n-2}\\ + \ x_{n-1} \\ + \ x_{n} \\ + \ y_0 \\ + \ y_1 \\ + \ y_2 \\ + \vdots \\ + \ y_{n-2}\\ + \ y_{n-1} \\ + \ y_{n} \\ + \end{pmatrix} +\end{align} $$ ### Model predictive trajectory @@ -152,7 +263,7 @@ When the optimization failed or the optimized trajectory is not collision free, Trajectory near the ego must be stable, therefore the condition where trajectory points near the ego are the same as previously generated trajectory is considered, and this is the only hard constraints in MPT. -#### Formulation +#### Vehicle kinematics As the following figure, we consider the bicycle kinematics model in the frenet frame to track the reference path. At time step $k$, we define lateral distance to the reference path, heading angle against the reference path, and steer angle as $y_k$, $\theta_k$, and $\delta_k$ respectively. @@ -170,6 +281,8 @@ y_{k+1} & = y_{k} + v \sin \theta_k dt \\ \end{align} $$ +##### Linearization + Then we linearize these equations. $y_k$ and $\theta_k$ are tracking errors, so we assume that those are small enough. Therefore $\sin \theta_k \approx \theta_k$. @@ -200,47 +313,197 @@ $$ \end{align} $$ -Based on the linearization, the error kinematics is formulated with the following linear equations. +##### One-step state equation + +Based on the linearization, the error kinematics is formulated with the following linear equations, $$ \begin{align} \begin{pmatrix} y_{k+1} \\ - \theta_{k+1} \\ - \delta_{k+1} + \theta_{k+1} \end{pmatrix} = \begin{pmatrix} - 1 & v dt & 0 \\ - 0 & 1 & \frac{v dt}{L \cos^{2} \delta_{\mathrm{ref}, k}} \\ - 0 & 0 & 1 - \frac{dt}{\tau} + 1 & v dt \\ + 0 & 1 \\ \end{pmatrix} \begin{pmatrix} y_k \\ \theta_k \\ - \delta_k \end{pmatrix} + \begin{pmatrix} 0 \\ - 0 \\ - \frac{dt}{\tau} + \frac{v dt}{L \cos^{2} \delta_{\mathrm{ref}, k}} \\ \end{pmatrix} - \delta_{des} + \delta_{k} + \begin{pmatrix} 0 \\ \frac{v \tan(\delta_{\mathrm{ref}, k}) dt}{L} - \frac{v \delta_{\mathrm{ref}, k} dt}{L \cos^{2} \delta_{\mathrm{ref}, k}} - \kappa_k v dt\\ - 0 \end{pmatrix} \end{align} $$ -The objective function for smoothing and tracking is shown as follows. +which can be formulated as follows with the state $\boldsymbol{x}$, control input $u$ and some matrices, where $\boldsymbol{x} = (y_k, \theta_k)$ + +$$ +\begin{align} + \boldsymbol{x}_{k+1} = A_k \boldsymbol{x}_k + \boldsymbol{b}_k u_k + \boldsymbol{w}_k +\end{align} +$$ + +##### Time-series state equation + +Then, we formulate time-series state equation by concatenating states, control inputs and matrices respectively as + +$$ +\begin{align} + \boldsymbol{x} = A \boldsymbol{x}_0 + B \boldsymbol{u} + \boldsymbol{w} +\end{align} +$$ + +where + +$$ +\begin{align} +\boldsymbol{x} = (\boldsymbol{x}^T_1, \boldsymbol{x}^T_2, \boldsymbol{x}^T_3, \dots, \boldsymbol{x}^T_{n-1})^T \\ +\boldsymbol{u} = (u_0, u_1, u_2, \dots, u_{n-2})^T \\ +\boldsymbol{w} = (\boldsymbol{w}^T_0, \boldsymbol{w}^T_1, \boldsymbol{w}^T_2, \dots, \boldsymbol{w}^T_{n-1})^T. \\ +\end{align} +$$ + +In detail, each matrices are constructed as follows. + +$$ +\begin{align} + \begin{pmatrix} + \boldsymbol{x}_1 \\ + \boldsymbol{x}_2 \\ + \boldsymbol{x}_3 \\ + \vdots \\ + \boldsymbol{x}_{n-1} + \end{pmatrix} + = + \begin{pmatrix} + A_0 \\ + A_1 A_0 \\ + A_2 A_1 A_0\\ + \vdots \\ + \prod\limits_{k=0}^{n-1} A_{k} + \end{pmatrix} + \boldsymbol{x}_0 + + + \begin{pmatrix} + B_0 & 0 & & \dots & 0 \\ + A_0 B_0 & B_1 & 0 & \dots & 0 \\ + A_1 A_0 B_0 & A_0 B_1 & B_2 & \dots & 0 \\ + \vdots & \vdots & & \ddots & 0 \\ + \prod\limits_{k=0}^{n-3} A_k B_0 & \prod\limits_{k=0}^{n-4} A_k B_1 & \dots & A_0 B_{n-3} & B_{n-2} + \end{pmatrix} + \begin{pmatrix} + u_0 \\ + u_1 \\ + u_2 \\ + \vdots \\ + u_{n-2} + \end{pmatrix} + + + \begin{pmatrix} + I & 0 & & \dots & 0 \\ + A_0 & I & 0 & \dots & 0 \\ + A_1 A_0 & A_0 & I & \dots & 0 \\ + \vdots & \vdots & & \ddots & 0 \\ + \prod\limits_{k=0}^{n-3} A_k & \prod\limits_{k=0}^{n-4} A_k & \dots & A_0 & I + \end{pmatrix} + \begin{pmatrix} + \boldsymbol{w}_0 \\ + \boldsymbol{w}_1 \\ + \boldsymbol{w}_2 \\ + \vdots \\ + \boldsymbol{w}_{n-2} + \end{pmatrix} +\end{align} +$$ + +##### Free-boundary-conditioned time-series state equation + +For path planning which does not start from the current ego pose, $\boldsymbol{x}_0$ should be the design variable of optimization. +Therefore, we make $\boldsymbol{u}'$ by concatenating $\boldsymbol{x}_0$ and $\boldsymbol{u}$, and redefine $\boldsymbol{x}$ as follows. + +$$ +\begin{align} + \boldsymbol{u}' & = (\boldsymbol{x}^T_0, \boldsymbol{u}^T)^T \\ + \boldsymbol{x} & = (\boldsymbol{x}^T_0, \boldsymbol{x}^T_1, \boldsymbol{x}^T_2, \dots, \boldsymbol{x}^T_{n-1})^T +\end{align} +$$ + +Then we get the following state equation + +$$ +\begin{align} + \boldsymbol{x}' = B \boldsymbol{u}' + \boldsymbol{w}, +\end{align} +$$ + +which is in detail $$ \begin{align} -J_1 & (y_{0...N-1}, \theta_{0...N-1}, \delta_{0...N-1}) \\ & = w_y \sum_{k} y_k^2 + w_{\theta} \sum_{k} \theta_k^2 + w_{\delta} \sum_k \delta_k^2 + w_{\dot{\delta}} \sum_k \dot{\delta}_k^2 + w_{\ddot{\delta}} \sum_k \ddot{\delta}_k^2 + \begin{pmatrix} + \boldsymbol{x}_0 \\ + \boldsymbol{x}_1 \\ + \boldsymbol{x}_2 \\ + \boldsymbol{x}_3 \\ + \vdots \\ + \boldsymbol{x}_{n-1} + \end{pmatrix} + = + \begin{pmatrix} + I & 0 & \dots & & & 0 \\ + A_0 & B_0 & 0 & & \dots & 0 \\ + A_1 A_0 & A_0 B_0 & B_1 & 0 & \dots & 0 \\ + A_2 A_1 A_0 & A_1 A_0 B_0 & A_0 B_1 & B_2 & \dots & 0 \\ + \vdots & \vdots & \vdots & & \ddots & 0 \\ + \prod\limits_{k=0}^{n-1} A_k & \prod\limits_{k=0}^{n-3} A_k B_0 & \prod\limits_{k=0}^{n-4} A_k B_1 & \dots & A_0 B_{n-3} & B_{n-2} + \end{pmatrix} + \begin{pmatrix} + \boldsymbol{x}_0 \\ + u_0 \\ + u_1 \\ + u_2 \\ + \vdots \\ + u_{n-2} + \end{pmatrix} + + + \begin{pmatrix} + 0 & \dots & & & 0 \\ + I & 0 & & \dots & 0 \\ + A_0 & I & 0 & \dots & 0 \\ + A_1 A_0 & A_0 & I & \dots & 0 \\ + \vdots & \vdots & & \ddots & 0 \\ + \prod\limits_{k=0}^{n-3} A_k & \prod\limits_{k=0}^{n-4} A_k & \dots & A_0 & I + \end{pmatrix} + \begin{pmatrix} + \boldsymbol{w}_0 \\ + \boldsymbol{w}_1 \\ + \boldsymbol{w}_2 \\ + \vdots \\ + \boldsymbol{w}_{n-2} + \end{pmatrix}. +\end{align} +$$ + +#### Objective function + +The objective function for smoothing and tracking is shown as follows, which can be formulated with value function matrices $Q, R$. + +$$ +\begin{align} +J_1 (\boldsymbol{x}', \boldsymbol{u}') & = w_y \sum_{k} y_k^2 + w_{\theta} \sum_{k} \theta_k^2 + w_{\delta} \sum_k \delta_k^2 + w_{\dot{\delta}} \sum_k \dot{\delta}_k^2 + w_{\ddot{\delta}} \sum_k \ddot{\delta}_k^2 \\ +& = \boldsymbol{x}'^T Q \boldsymbol{x}' + \boldsymbol{u}'^T R \boldsymbol{u}' \\ +& = \boldsymbol{u}'^T H \boldsymbol{u}' + \boldsymbol{u}'^T \boldsymbol{f} \end{align} $$ @@ -250,25 +513,29 @@ Assuming that the lateral distance to the road boundaries or obstacles from the $$ y_{\mathrm{base}, k, \min} - \lambda_{\mathrm{base}, k} \leq y_{\mathrm{base}, k} (y_k) \leq y_{\mathrm{base}, k, \max} + \lambda_{\mathrm{base}, k}\\ y_{\mathrm{top}, k, \min} - \lambda_{\mathrm{top}, k} \leq y_{\mathrm{top}, k} (y_k) \leq y_{\mathrm{top}, k, \max} + \lambda_{\mathrm{top}, k}\\ -y_{\mathrm{mid}, k, \min} - \lambda_{\mathrm{mid}, k} \leq y_{\mathrm{mid}, k} (y_k) \leq y_{\mathrm{mid}, k, \max} + \lambda_{\mathrm{mid}, k} +y_{\mathrm{mid}, k, \min} - \lambda_{\mathrm{mid}, k} \leq y_{\mathrm{mid}, k} (y_k) \leq y_{\mathrm{mid}, k, \max} + \lambda_{\mathrm{mid}, k} \\ +0 \leq \lambda_{\mathrm{base}, k} \\ +0 \leq \lambda_{\mathrm{top}, k} \\ +0 \leq \lambda_{\mathrm{mid}, k} $$ Since $y_{\mathrm{base}, k}, y_{\mathrm{top}, k}, y_{\mathrm{mid}, k}$ is formulated as a linear function of $y_k$, the objective function for soft constraints is formulated as follows. $$ \begin{align} -J_2 & (\lambda_{\mathrm{base}, 0...N-1}, \lambda_{\mathrm{mid}, 0...N-1}, \lambda_{\mathrm{top}, 0...N-1}) \\ & = w_{\mathrm{base}} \sum_{k} \lambda_{\mathrm{base}, k}^2 + w_{\mathrm{mid}} \sum_k \lambda_{\mathrm{mid}, k}^2 + w_{\mathrm{top}} \sum_k \lambda_{\mathrm{top}, k}^2 +J_2 & (\boldsymbol{\lambda}_\mathrm{base}, \boldsymbol{\lambda}_\mathrm{top}, \boldsymbol {\lambda}_\mathrm{mid})\\ +& = w_{\mathrm{base}} \sum_{k} \lambda_{\mathrm{base}, k} + w_{\mathrm{mid}} \sum_k \lambda_{\mathrm{mid}, k} + w_{\mathrm{top}} \sum_k \lambda_{\mathrm{top}, k} \end{align} $$ Slack variables are also design variables for optimization. -We define a vector $\boldsymbol{x}$, that concatenates all the design variables. +We define a vector $\boldsymbol{v}$, that concatenates all the design variables. $$ \begin{align} -\boldsymbol{x} = +\boldsymbol{v} = \begin{pmatrix} -... & y_k & \lambda_{\mathrm{base}, k} & \lambda_{\mathrm{top}, k} & \lambda_{\mathrm{mid}, k} & ... + \boldsymbol{u}'^T & \boldsymbol{\lambda}_\mathrm{base}^T & \boldsymbol{\lambda}_\mathrm{top}^T & \boldsymbol{\lambda}_\mathrm{mid}^T \end{pmatrix}^T \end{align} $$ @@ -277,7 +544,7 @@ The summation of these two objective functions is the objective function for the $$ \begin{align} -\min_{\boldsymbol{x}} J (\boldsymbol{x}) = \min_{\boldsymbol{x}} J_1 & (y_{0...N-1}, \theta_{0...N-1}, \delta_{0...N-1}) + J_2 (\lambda_{\mathrm{base}, 0...N-1}, \lambda_{\mathrm{mid}, 0...N-1}, \lambda_{\mathrm{top}, 0...N-1}) +\min_{\boldsymbol{v}} J (\boldsymbol{v}) = \min_{\boldsymbol{v}} J_1 (\boldsymbol{u}') + J_2 (\boldsymbol{\lambda}_\mathrm{base}, \boldsymbol{\lambda}_\mathrm{top}, \boldsymbol{\lambda}_\mathrm{mid}) \end{align} $$ @@ -294,11 +561,215 @@ Finally we transform those objective functions to the following QP problem, and $$ \begin{align} -\min_{\boldsymbol{x}} \ & \frac{1}{2} \boldsymbol{x}^T \boldsymbol{P} \boldsymbol{x} + \boldsymbol{q} \boldsymbol{x} \\ -\mathrm{s.t.} \ & \boldsymbol{b}_l \leq \boldsymbol{A} \boldsymbol{x} \leq \boldsymbol{b}_u +\min_{\boldsymbol{v}} \ & \frac{1}{2} \boldsymbol{v}^T \boldsymbol{H} \boldsymbol{v} + \boldsymbol{f} \boldsymbol{v} \\ +\mathrm{s.t.} \ & \boldsymbol{b}_{lower} \leq \boldsymbol{A} \boldsymbol{v} \leq \boldsymbol{b}_{upper} +\end{align} +$$ + +#### Constraints + +##### Steer angle limitation + +Steer angle has a certain limitation ($\delta_{max}$, $\delta_{min}$). +Therefore we add linear inequality equations. + +$$ +\begin{align} +\delta_{min} \leq \delta_i \leq \delta_{max} +\end{align} +$$ + +##### Collision free + +To realize collision-free path planning, we have to formulate constraints that the vehicle is inside the road (moreover, a certain meter far from the road boundary) and does not collide with obstacles in linear equations. +For linearity, we chose a method to approximate the vehicle shape with a set of circles, that is reliable and easy to implement. + +Now we formulate the linear constraints where a set of circles on each trajectory point is collision-free. +For collision checking, we have a drivable area in the format of an image where walls or obstacles are filled with a color. +By using this drivable area, we calculate upper (left) and lower (right) boundaries along reference points so that we can interpolate boundaries on any position on the trajectory. + +Assuming that upper and lower boundaries are $b_l$, $b_u$ respectively, and $r$ is a radius of a circle, lateral deviation of the circle center $y'$ has to be + +$$ +b_l + r \leq y' \leq b_u - r. +$$ + +Based on the following figure, $y'$ can be formulated as follows. + +$$ +\begin{align} +y' & = L \sin(\theta + \beta) + y \cos \beta - l \sin(\gamma - \phi_a) \\ +& = L \sin \theta \cos \beta + L \cos \theta \sin \beta + y \cos \beta - l \sin(\gamma - \phi_a) \\ +& \approx L \theta \cos \beta + L \sin \beta + y \cos \beta - l \sin(\gamma - \phi_a) +\end{align} +$$ + +$$ +b_l + r - \lambda \leq y' \leq b_u - r + \lambda. +$$ + +$$ +\begin{align} +y' & = C_1 \boldsymbol{x} + C_2 \\ +& = C_1 (B \boldsymbol{v} + \boldsymbol{w}) + C_2 \\ +& = C_1 B \boldsymbol{v} + \boldsymbol{w} + C_2 \end{align} $$ +Note that longitudinal position of the circle center and the trajectory point to calculate boundaries are different. +But each boundaries are vertical against the trajectory, resulting in less distortion by the longitudinal position difference since road boundaries does not change so much. +For example, if the boundaries are not vertical against the trajectory and there is a certain difference of longitudinal position between the circe center and the trajectory point, we can easily guess that there is much more distortion when comparing lateral deviation and boundaries. + +$$ +\begin{align} + A_{blk} & = + \begin{pmatrix} + C_1 B & O & \dots & O & I_{N_{ref} \times N_{ref}} & O \dots & O\\ + -C_1 B & O & \dots & O & I & O \dots & O\\ + O & O & \dots & O & I & O \dots & O + \end{pmatrix} + \in \boldsymbol{R}^{3 N_{ref} \times D_v + N_{circle} N_{ref}} \\ + \boldsymbol{b}_{lower, blk} & = + \begin{pmatrix} + \boldsymbol{b}_{lower} - C_1 \boldsymbol{w} - C_2 \\ + -\boldsymbol{b}_{upper} + C_1 \boldsymbol{w} + C_2 \\ + O + \end{pmatrix} + \in \boldsymbol{R}^{3 N_{ref}} \\ + \boldsymbol{b}_{upper, blk} & = \boldsymbol{\infty} + \in \boldsymbol{R}^{3 N_{ref}} +\end{align} +$$ + +We will explain options for optimization. + +###### L-infinity optimization + +The above formulation is called L2 norm for slack variables. +Instead, if we use L-infinity norm where slack variables are shared by enabling `l_inf_norm`. + +$$ +\begin{align} + A_{blk} = + \begin{pmatrix} + C_1 B & I_{N_{ref} \times N_{ref}} \\ + -C_1 B & I \\ + O & I + \end{pmatrix} +\in \boldsymbol{R}^{3N_{ref} \times D_v + N_{ref}} +\end{align} +$$ + +###### Two-step soft constraints + +$$ +\begin{align} +\boldsymbol{v}' = + \begin{pmatrix} + \boldsymbol{v} \\ + \boldsymbol{\lambda}^{soft_1} \\ + \boldsymbol{\lambda}^{soft_2} \\ + \end{pmatrix} + \in \boldsymbol{R}^{D_v + 2N_{slack}} +\end{align} +$$ + +$*$ depends on whether to use L2 norm or L-infinity optimization. + +$$ +\begin{align} + A_{blk} & = + \begin{pmatrix} + A^{soft_1}_{blk} \\ + A^{soft_2}_{blk} \\ + \end{pmatrix}\\ + & = + \begin{pmatrix} + C_1^{soft_1} B & & \\ + -C_1^{soft_1} B & \Huge{*} & \Huge{O} \\ + O & & \\ + C_1^{soft_2} B & & \\ + -C_1^{soft_2} B & \Huge{O} & \Huge{*} \\ + O & & + \end{pmatrix} + \in \boldsymbol{R}^{6 N_{ref} \times D_v + 2 N_{slack}} +\end{align} +$$ + +$N_{slack}$ is $N_{circle}$ when L2 optimization, or $1$ when L-infinity optimization. +$N_{circle}$ is the number of circles to check collision. + +## Tuning + +### Vehicle + +- max steering wheel degree + - `mpt.kinematics.max_steer_deg` + +### Assumptions + +- EB optimized trajectory length should be longer than MPT optimized trajectory length + - since MPT result may be jerky because of non-fixed reference path (= EB optimized trajectory) + - At least, EB fixed optimized trajectory length must be longer than MPT fixed optimization trajectory length + - This causes the case that there is a large difference between MPT fixed optimized point and MPT optimized point just after the point. + +### Drivability in narrow roads + +- set `option.drivability_check.use_vehicle_circles` true + - use a set of circles as a shape of the vehicle when checking if the generated trajectory will be outside the drivable area. +- make `mpt.clearance.soft_clearance_from_road` smaller +- make `mpt.kinematics.optimization_center_offset` different + + - The point on the vehicle, offset forward from the base link` tries to follow the reference path. + + - This may cause the a part of generated trajectory will be outside the drivable area. + +### Computation time + +- Loose EB optimization + + - 1. make `eb.common.delta_arc_length_for_eb` large and `eb.common.num_sampling_points_for_eb` small + - This makes the number of design variables smaller + - Be careful about the trajectory length between MPT and EB as shown in Assumptions. + - However, empirically this causes large turn at the corner (e.g. The vehicle turns a steering wheel to the opposite side (=left) a bit just before the corner turning to right) + - 2. make `eb.qp.eps_abs` and `eb.qp.eps_rel` small + - This causes very unstable reference path generation for MPT, or turning a steering wheel a little bit larger + +- Enable computation reduction flag + + - 1. set l_inf_norm true (by default) + - use L-inf norm optimization for MPT w.r.t. slack variables, resulting in lower number of design variables + - 2. set enable_warm_start true + - 3. set enable_manual_warm_start true (by default) + - 4. set steer_limit_constraint false + - This causes no assumption for trajectory generation where steering angle will not exceeds its hardware limitation + - 5. make the number of collision-free constraints small + - How to change parameters depend on the type of collision-free constraints + - If + - This may cause the trajectory generation where a part of ego vehicle is out of drivable area + +- Disable publishing debug visualization markers + - set `option.is_publishing_*` false + +### Robustness + +- Check if the trajectory before EB, after EB, or after MPT is not robust + - if the trajectory before EB is not robust + - if the trajectory after EB is not robust + - if the trajectory after MPT is not robust + - make `mpt.weight.steer_input_weight` or `mpt.weight.steer_rate_weight` larger, which are stability of steering wheel along the trajectory. + +### Other options + +- `option.skip_optimization` skips EB and MPT optimization. +- `option.enable_pre_smoothing` enables EB which is smoothing the trajectory for MPT. + - EB is not required if the reference path for MPT is smooth enough and does not change its shape suddenly +- `option.is_showing_calculation_time` enables showing each calculation time for functions and total calculation time on the terminal. +- `option.is_stopping_if_outside_drivable_area` enables stopping just before the generated trajectory point will be outside the drivable area. +- `mpt.option.plan_from_ego` enables planning from the ego pose when the ego's velocity is zero. +- `mpt.option.two_step_soft_constraint` enables two step of soft constraints for collision free + - `mpt.option.soft_clearance_from_road` and `mpt.option.soft_second_clearance_from_road` are the weight. + ## Limitation - When turning right or left in the intersection, the output trajectory is close to the outside road boundary. @@ -331,54 +802,51 @@ Although it has a cons to converge to the local minima, it can get a good soluti Topics for debugging will be explained in this section. -- **interpolated_points_marker** - - Trajectory points that is resampled from the input trajectory of obstacle avoidance planner. Whether this trajectory is inside the drivable area, smooth enough can be checked. - -![interpolated_points_marker](./media/interpolated_points_marker.png) - -- **smoothed_points_text** - - The output trajectory points from Elastic Band. Not points but small numbers are visualized. Whether these smoothed points are distorted or not can be checked. +- **Drivable area** + - Drivable area to cover the road. Whether this area is continuous and covers the road can be checked. + - `/planning/scenario_planning/lane_driving/behavior_planning/behavior_path_planner/debug/drivable_area`, whose type is `nav_msgs/msg/OccupancyGrid` -![smoothed_points_text](./media/smoothed_points_text.png) +![drivable_area](./media/drivable_area.png) -- **(base/top/mid)\_bounds_line** - - Lateral Distance to the road boundaries to check collision in MPT (More precisely, - vehicle_width / 2.0). - - This collision check is done with three points on the vehicle (base = back wheel center, top, mid), therefore three line markers are visualized for each trajectory point. - - If the distance between the edge of line markers and the road boundaries is not about the half width of the vehicle, collision check will fail. +- **Path from behavior** + - The input path of obstacle_avoidance_planner. Whether this path is continuous and the curvature is not so high can be checked. + - `Path` or `PathFootprint` rviz plugin. -![bounds_line](./media/bounds_line.png) +![behavior_path](./media/behavior_path.png) -- **optimized_points_marker** - - The output trajectory points from MPT. Whether the trajectory is outside the drivable area can be checked. +- **EB trajectory** + - The output trajectory of elastic band. Whether this trajectory is very smooth and a sampling width is constant can be checked. + - `Trajectory` or `TrajectoryFootprint` rviz plugin. -![optimized_points_marker](./media/optimized_points_marker.png) +![eb_traj](./media/eb_traj.png) -- **Trajectory with footprint** - - Trajectory footprints can be visualized by TrajectoryFootprint of rviz_plugin. Whether trajectory footprints of input/output of obstacle_avoidance_planner is inside the drivable area or not can be checked. +- **MPT reference trajectory** + - The reference trajectory of model predictive trajectory. Whether this trajectory is very smooth and a sampling width is constant can be checked. + - `Trajectory` or `TrajectoryFootprint` rviz plugin. -![trajectory_with_footprint](./media/trajectory_with_footprint.png) +![mpt_ref_traj](./media/mpt_ref_traj.png) -- **Drivable Area** - - Drivable area. When drivable area generation failed, the drawn area may be distorted. - - `nav_msgs/msg/OccupancyGrid` - - topic name: `/planning/scenario_planning/lane_driving/behavior_planning/behavior_path_planner/debug/drivable_area` +- **MPT fixed trajectory** + - The fixed trajectory around the ego of model predictive trajectory. + - `Trajectory` or `TrajectoryFootprint` rviz plugin. -![drivable_area](./media/drivable_area.png) +![mpt_fixed_traj](./media/mpt_fixed_traj.png) -- **area_with_objects** - - Area where obstacles are removed from the drivable area - - `nav_msgs/msg/OccupancyGrid` +- **bounds** + - Lateral Distance to the road or object boundaries to check collision in model predictive trajectory. + - Whether these lines' ends align the road or obstacle boundaries can be checked. + - `bounds*` of `/planning/scenario_planning/lane_driving/motion_planning/obstacle_avoidance_planner/debug/marker` whose type is `visualization_msgs/msg/MarkerArray` -![area_with_objects](./media/area_with_objects.png) +![bounds](./media/bounds.png) -- **object_clearance_map** - - Cost map for obstacles (distance to the target obstacles to be avoided) - - `nav_msgs/msg/OccupancyGrid` +- **MPT trajectory** + - The output of model predictive trajectory. Whether this trajectory is smooth enough and inside the drivable area can be checked. + - `Trajectory` or `TrajectoryFootprint` rviz plugin. -![object_clearance_map](./media/object_clearance_map.png) +![mpt_traj](./media/mpt_traj.png) -- **clearance_map** - - Cost map for drivable area (distance to the road boundaries) - - `nav_msgs/msg/OccupancyGrid` +- **Output trajectory** + - The output of obstacle_avoidance_planner. Whether this trajectory is smooth enough can be checked. + - `Trajectory` or `TrajectoryFootprint` rviz plugin. -![clearance_map](./media/clearance_map.png) +![output_traj](./media/output_traj.png) diff --git a/planning/obstacle_avoidance_planner/config/obstacle_avoidance_planner.param.yaml b/planning/obstacle_avoidance_planner/config/obstacle_avoidance_planner.param.yaml index 11e1f8a905f12..40b64b7d87012 100644 --- a/planning/obstacle_avoidance_planner/config/obstacle_avoidance_planner.param.yaml +++ b/planning/obstacle_avoidance_planner/config/obstacle_avoidance_planner.param.yaml @@ -1,99 +1,173 @@ /**: ros__parameters: - # trajectory total/fixing length - trajectory_length: 200 # total trajectory length[m] - forward_fixing_distance: 20.0 # forward fixing length from base_link[m] - backward_fixing_distance: 5.0 # backward fixing length from base_link[m] - - # clearance(distance) when generating trajectory - clearance_from_road: 0.2 # clearance from road boundary[m] - clearance_from_object: 1.0 # clearance from object[m] - min_object_clearance_for_joint: 3.2 # minimum clearance from object[m] when judging is_including_only_smooth_range - extra_desired_clearance_from_road: 0.0 # extra desired clearance from road - - # clearance for unique points - clearance_for_straight_line: 0.05 # minimum optimizing range around straight points - clearance_for_joint: 0.1 # minimum optimizing range around joint points - clearance_for_only_smoothing: 0.1 # minimum optimizing range when applying only smoothing - range_for_extend_joint: 0.1 # minimum optimizing range around joint points for extending - clearance_from_object_for_straight: 10.0 # minimum clearance from object when judging straight line - - # avoiding param - max_avoiding_objects_velocity_ms: 0.5 # maximum velocity for avoiding objects[m/s] - max_avoiding_ego_velocity_ms: 6.0 # maximum ego velocity when avoiding objects[m/s] - center_line_width: 1.7 # replaced with /vehicle_info/vehicle_width when is_using_vehicle_config is true. center line width around path points used for judging that object is required to avoid or not - acceleration_for_non_deceleration_range: 1.0 # assumed acceleration for calculating non deceleration range - avoiding_object_type: # avoiding object class - unknown: true - car: true - truck: true - bus: true - bicycle: true - motorbike: true - pedestrian: true - animal: true - - # solving quadratic programming - qp_max_iteration: 10000 # max iteration when solving QP - qp_eps_abs: 1.0e-8 # eps abs when solving OSQP - qp_eps_rel: 1.0e-11 # eps rel when solving OSQP - qp_eps_abs_for_extending: 1.0e-8 # eps abs when solving OSQP for extending - qp_eps_rel_for_extending: 1.0e-9 # eps rel when solving OSQP for extending - qp_eps_abs_for_visualizing: 1.0e-6 # eps abs when solving OSQP for visualizing - qp_eps_rel_for_visualizing: 1.0e-8 # eps rel when solving OSQP for visualizing - - # constrain space - is_getting_constraints_close2path_points: true # generate trajectory closer to path points - max_x_constrain_search_range: 0.0 # maximum x search range in local coordinate - coef_x_constrain_search_resolution: 1.0 # coef for fine sampling when exploring x direction - coef_y_constrain_search_resolution: 0.5 # coef for fine sampling when exploring y direction - keep_space_shape_x: 0.2 # keep space for x direction from base_link[m] - keep_space_shape_y: 0.1 # replaced with /vehicle_info/vehicle_width when is_using_vehicle_config is true. keep space for y direction from base_link[m] - max_lon_space_for_driveable_constraint: 0.5 # maximum lon(x) space when optimizing point[m] - - is_publishing_clearance_map: true # publish clearance map as nav_msgs::OccupancyGrid - is_publishing_area_with_objects: true # publish occupancy map as nav_msgs::OccupancyGrid - enable_avoidance: true # enable avoidance function - is_using_vehicle_config: true # use vehicle config - num_sampling_points: 100 # number of optimizing points - num_joint_buffer_points: 3 # number of joint buffer points - num_joint_buffer_points_for_extending: 6 # number of joint buffer points for extending - num_offset_for_begin_idx: 2 # number of shifting points for beginning non-fix idx - num_fix_points_for_extending: 50 # number of fixing points when extending - delta_arc_length_for_optimization: 1.0 # delta arc length when optimizing[m] - delta_arc_length_for_trajectory: 0.1 # delta arc length for trajectory[m] - delta_dist_threshold_for_closest_point: 3.0 # delta dist thres for closest point[m] - delta_yaw_threshold_for_closest_point: 1.046 #M_PI/3.0. delta yaw thres for closest point - delta_yaw_threshold_for_straight: 0.02 # delta dist thres for straight point + option: + # publish + is_publishing_debug_visualization_marker: true + is_publishing_clearance_map: false # publish clearance map as nav_msgs::OccupancyGrid + is_publishing_object_clearance_map: false # publish clearance map as nav_msgs::OccupancyGrid + is_publishing_area_with_objects: false # publish occupancy map as nav_msgs::OccupancyGrid + + is_stopping_if_outside_drivable_area: true # stop if the ego's footprint will be outside the drivable area + + # show + is_showing_debug_info: false + is_showing_calculation_time: false + + # other + enable_avoidance: false # enable avoidance function + enable_pre_smoothing: true # enable EB + skip_optimization: false # skip MPT and EB + reset_prev_optimization: false + + common: + # sampling + num_sampling_points: 100 # number of optimizing points + + # trajectory total/fixing length + trajectory_length: 300.0 # total trajectory length[m] + + forward_fixing_min_distance: 1.0 # number of fixing points around ego vehicle [m] + forward_fixing_min_time: 0.5 # forward fixing time with current velocity [s] + + backward_fixing_distance: 5.0 # backward fixing length from base_link [m] + delta_arc_length_for_trajectory: 0.1 # delta arc length for trajectory [m] + + delta_dist_threshold_for_closest_point: 3.0 # delta dist thres for closest point [m] + delta_yaw_threshold_for_closest_point: 1.046 #M_PI/3.0, delta yaw thres for closest point + delta_yaw_threshold_for_straight: 0.02 # delta dist thres for straight point + + num_fix_points_for_extending: 50 # number of fixing points when extending + max_dist_for_extending_end_point: 0.0001 # minimum delta dist thres for extending last point [m] + + object: # avoiding object + max_avoiding_objects_velocity_ms: 0.5 # maximum velocity for avoiding objects [m/s] + max_avoiding_ego_velocity_ms: 6.0 # maximum ego velocity when avoiding objects [m/s] + + avoiding_object_type: + unknown: true + car: true + truck: true + bus: true + bicycle: true + motorbike: true + pedestrian: true + animal: true # mpt param - # vehicle param for mpt - max_steer_deg: 40.0 # max steering angle [deg] - steer_tau: 0.1 # steering dynamics time constant (1d approximation) [s] - - # trajectory param for mpt - num_curvature_sampling_points: 5 # number of sampling points when calculating curvature - delta_arc_length_for_mpt_points: 0.5 # delta arc length when generating mpt[m] - forward_fixing_mpt_distance: 10 # forward fixing distance for MPT - - # optimization param for mpt - is_hard_fixing_terminal_point: false # hard fixing terminal point - base_point_weight: 2000.0 # slack weight for lateral error around base_link - top_point_weight: 1000.0 # slack weight for lateral error around vehicle-top point - mid_point_weight: 1000.0 # slack weight for lateral error around the middle point - lat_error_weight: 10.0 # weight for lateral error - yaw_error_weight: 0.0 # weight for yaw error - steer_input_weight: 0.01 # weight for steering input - steer_rate_weight: 1.0 # weight for steering rate - steer_acc_weight: 0.000001 # weight for steering acceleration - terminal_lat_error_weight: 0.0 # weight for lateral error at terminal point - terminal_yaw_error_weight: 100.0 # weight for yaw error at terminal point - terminal_path_lat_error_weight: 1000.0 # weight for lateral error at path end point - terminal_path_yaw_error_weight: 1000.0 # weight for yaw error at path end point - zero_ff_steer_angle: 0.5 # threshold that feed-forward angle becomes zero + mpt: + option: + steer_limit_constraint: true + fix_points_around_ego: true + # plan_from_ego: false + visualize_sampling_num: 1 + enable_manual_warm_start: true + enable_warm_start: true # false + + common: + num_curvature_sampling_points: 5 # number of sampling points when calculating curvature + delta_arc_length_for_mpt_points: 0.5 # delta arc length when generating mpt [m] + + kinematics: + max_steer_deg: 40.0 # max steering angle [deg] + + # If this parameter is commented out, the parameter is set as below by default. + # The logic could be `optimization_center_offset = vehicle_info.wheel_base * 0.8` + # The 0.8 scale is adopted as it performed the best. + # optimization_center_offset: 2.3 # optimization center offset from base link # replanning & trimming trajectory param outside algorithm - min_delta_dist_for_replan: 10.0 # minimum delta dist thres for replan[m] - min_delta_time_sec_for_replan: 1.0 # minimum delta time for replan[second] - max_dist_for_extending_end_point: 5.0 # minimum delta dist thres for extending last point[m] - distance_for_path_shape_change_detection: 2.0 # minimum delta dist thres for detecting path shape change + replan: + max_path_shape_change_dist: 0.3 # threshold of path shape change from behavior path [m] + max_ego_moving_dist_for_replan: 3.0 # threshold of ego's moving distance for replan [m] + max_delta_time_sec_for_replan: 1.0 # threshold of delta time for replan [second] + + # advanced parameters to improve performance as much as possible + advanced: + option: + # check if planned trajectory is outside drivable area + drivability_check: + # true: vehicle shape is considered as a set of circles + # false: vehicle shape is considered as footprint (= rectangle) + use_vehicle_circles: false + + # parameters only when use_vehicle_circles is true + vehicle_circles: + use_manual_vehicle_circles: false + num_for_constraints: 4 + + # parameters only when use_manual_vehicle_circles is true + longitudinal_offsets: [-0.15, 0.88, 1.9, 2.95] + radius: 0.95 + + # parameters only when use_manual_vehicle_circles is false + radius_ratio: 0.9 + # If this parameter is commented out, the parameter is calculated automatically + # based on the vehicle length and width + # num_for_radius: 4 + + eb: + common: + num_joint_buffer_points: 3 # number of joint buffer points + num_offset_for_begin_idx: 2 # number of shifting points for beginning non-fix idx + delta_arc_length_for_eb: 0.6 # 1.0 # delta arc length when optimizing[m] When the value is around 1.0, overshoot at the corner happens. + num_sampling_points_for_eb: 95 # number of optimizing points # must be longer than mpt points + + clearance: + clearance_for_straight_line: 0.05 # minimum optimizing range around straight points + clearance_for_joint: 0.1 # minimum optimizing range around joint points + clearance_for_only_smoothing: 0.1 # minimum optimizing range when applying only smoothing + + qp: + max_iteration: 10000 # max iteration when solving QP + eps_abs: 1.0e-8 # eps abs when solving OSQP + eps_rel: 1.0e-10 # eps rel when solving OSQP + + mpt: + clearance: # clearance(distance) between vehicle and roads/objects when generating trajectory + hard_clearance_from_road: 0.0 # clearance from road boundary[m] + soft_clearance_from_road: 0.1 # clearance from road boundary[m] + soft_second_clearance_from_road: 1.0 # clearance from road boundary[m] + clearance_from_object: 1.0 # clearance from object[m] + extra_desired_clearance_from_road: 0.0 # extra desired clearance from road + + weight: + soft_avoidance_weight: 1000.0 # slack weight for lateral error around the middle point + soft_second_avoidance_weight: 100.0 # slack weight for lateral error around the middle point + + lat_error_weight: 100.0 # weight for lateral error + yaw_error_weight: 0.0 # weight for yaw error + yaw_error_rate_weight: 0.0 # weight for yaw error rate + steer_input_weight: 10.0 # weight for steering input + steer_rate_weight: 10.0 # weight for steering rate + + obstacle_avoid_lat_error_weight: 3.0 # weight for lateral error + obstacle_avoid_yaw_error_weight: 0.0 # weight for yaw error + obstacle_avoid_steer_input_weight: 1000.0 # weight for yaw error + near_objects_length: 30.0 # weight for yaw error + + terminal_lat_error_weight: 100.0 # weight for lateral error at terminal point + terminal_yaw_error_weight: 100.0 # weight for yaw error at terminal point + terminal_path_lat_error_weight: 1000.0 # weight for lateral error at path end point + terminal_path_yaw_error_weight: 1000.0 # weight for yaw error at path end point + + # check if planned trajectory is outside drivable area + collision_free_constraints: + option: + l_inf_norm: true + soft_constraint: true + hard_constraint: false + # two_step_soft_constraint: false + + vehicle_circles: + use_manual_vehicle_circles: false + num_for_constraints: 3 + + # parameters only when use_manual_vehicle_circles is true + longitudinal_offsets: [-0.15, 0.88, 1.9, 2.95] + radius: 0.95 + + # parameters only when use_manual_vehicle_circles is false + radius_ratio: 0.8 + # If this parameter is commented out, the parameter is calculated automatically + # based on the vehicle length and width + # num_for_radius: 4 diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/common_structs.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/common_structs.hpp new file mode 100644 index 0000000000000..5eba99ae6dfef --- /dev/null +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/common_structs.hpp @@ -0,0 +1,255 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 OBSTACLE_AVOIDANCE_PLANNER__COMMON_STRUCTS_HPP_ +#define OBSTACLE_AVOIDANCE_PLANNER__COMMON_STRUCTS_HPP_ + +#include "opencv2/core.hpp" +#include "opencv2/opencv.hpp" +#include "rclcpp/rclcpp.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_object.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory_point.hpp" +#include "nav_msgs/msg/map_meta_data.hpp" + +#include + +#include +#include +#include + +struct ReferencePoint; + +struct Bounds; +using VehicleBounds = std::vector; +using SequentialBounds = std::vector; + +using BoundsCandidates = std::vector; +using SequentialBoundsCandidates = std::vector; + +struct CVMaps +{ + cv::Mat drivable_area; + cv::Mat clearance_map; + cv::Mat only_objects_clearance_map; + cv::Mat area_with_objects_map; + nav_msgs::msg::MapMetaData map_info; +}; + +struct QPParam +{ + int max_iteration; + double eps_abs; + double eps_rel; +}; + +struct EBParam +{ + double clearance_for_fixing; + double clearance_for_straight_line; + double clearance_for_joint; + double clearance_for_only_smoothing; + QPParam qp_param; + + int num_joint_buffer_points; + int num_offset_for_begin_idx; + + double delta_arc_length_for_eb; + int num_sampling_points_for_eb; +}; + +struct VehicleParam +{ + double wheelbase; + double length; + double width; + double rear_overhang; + double front_overhang; + // double max_steer_rad; +}; + +struct ConstrainRectangle +{ + geometry_msgs::msg::Point top_left; + geometry_msgs::msg::Point top_right; + geometry_msgs::msg::Point bottom_left; + geometry_msgs::msg::Point bottom_right; + double velocity; + bool is_empty_driveable_area = false; + bool is_including_only_smooth_range = true; +}; + +struct DebugData +{ + struct StreamWithPrint + { + StreamWithPrint & operator<<(const std::string & s) + { + sstream << s; + if (s.back() == '\n') { + std::string tmp_str = sstream.str(); + debug_str += tmp_str; + + if (is_showing_calculation_time) { + tmp_str.pop_back(); // NOTE* remove '\n' which is unnecessary for RCLCPP_INFO_STREAM + RCLCPP_INFO_STREAM(rclcpp::get_logger("obstacle_avoidance_planner.time"), tmp_str); + } + sstream.str(""); + } + return *this; + } + + StreamWithPrint & operator<<(const double d) + { + sstream << d; + return *this; + } + + std::string getString() const { return debug_str; } + + bool is_showing_calculation_time; + std::string debug_str = "\n"; + std::stringstream sstream; + }; + + void init( + const bool local_is_showing_calculation_time, const int local_mpt_visualize_sampling_num, + const geometry_msgs::msg::Pose & local_current_ego_pose, + const double local_vehicle_circle_radius, + const std::vector & local_vehicle_circle_longitudinal_offsets) + { + msg_stream.is_showing_calculation_time = local_is_showing_calculation_time; + mpt_visualize_sampling_num = local_mpt_visualize_sampling_num; + current_ego_pose = local_current_ego_pose; + vehicle_circle_radius = local_vehicle_circle_radius; + vehicle_circle_longitudinal_offsets = local_vehicle_circle_longitudinal_offsets; + } + + StreamWithPrint msg_stream; + size_t mpt_visualize_sampling_num; + geometry_msgs::msg::Pose current_ego_pose; + double vehicle_circle_radius; + std::vector vehicle_circle_longitudinal_offsets; + + boost::optional stop_pose_by_drivable_area = boost::none; + std::vector interpolated_points; + std::vector straight_points; + std::vector fixed_points; + std::vector non_fixed_points; + std::vector constrain_rectangles; + std::vector avoiding_traj_points; + std::vector avoiding_objects; + + cv::Mat clearance_map; + cv::Mat only_object_clearance_map; + cv::Mat area_with_objects_map; + + std::vector> vehicle_circles_pose; + std::vector ref_points; + + std::vector mpt_ref_poses; + std::vector lateral_errors; + + std::vector eb_traj; + std::vector mpt_fixed_traj; + std::vector mpt_ref_traj; + std::vector mpt_traj; + std::vector extended_fixed_traj; + std::vector extended_non_fixed_traj; +}; + +struct Trajectories +{ + std::vector smoothed_trajectory; + std::vector mpt_ref_points; + std::vector model_predictive_trajectory; +}; + +struct TrajectoryParam +{ + bool is_avoiding_unknown; + bool is_avoiding_car; + bool is_avoiding_truck; + bool is_avoiding_bus; + bool is_avoiding_bicycle; + bool is_avoiding_motorbike; + bool is_avoiding_pedestrian; + bool is_avoiding_animal; + int num_sampling_points; + double delta_arc_length_for_trajectory; + double delta_dist_threshold_for_closest_point; + double delta_yaw_threshold_for_closest_point; + double delta_yaw_threshold_for_straight; + double trajectory_length; + double forward_fixing_min_distance; + double forward_fixing_min_time; + double backward_fixing_distance; + double max_avoiding_ego_velocity_ms; + double max_avoiding_objects_velocity_ms; + double center_line_width; + double acceleration_for_non_deceleration_range; + int num_fix_points_for_extending; + double max_dist_for_extending_end_point; +}; + +struct MPTParam +{ + bool enable_warm_start; + bool enable_manual_warm_start; + bool steer_limit_constraint; + bool fix_points_around_ego; + int num_curvature_sampling_points; + + std::vector vehicle_circle_longitudinal_offsets; // from base_link + double vehicle_circle_radius; + + double delta_arc_length_for_mpt_points; + + double hard_clearance_from_road; + double soft_clearance_from_road; + double soft_second_clearance_from_road; + double extra_desired_clearance_from_road; + double clearance_from_object; + double soft_avoidance_weight; + double soft_second_avoidance_weight; + + double lat_error_weight; + double yaw_error_weight; + double yaw_error_rate_weight; + + double near_objects_length; + + double terminal_lat_error_weight; + double terminal_yaw_error_weight; + double terminal_path_lat_error_weight; + double terminal_path_yaw_error_weight; + + double steer_input_weight; + double steer_rate_weight; + + double obstacle_avoid_lat_error_weight; + double obstacle_avoid_yaw_error_weight; + double obstacle_avoid_steer_input_weight; + + double optimization_center_offset; + double max_steer_rad; + + bool soft_constraint; + bool hard_constraint; + bool l_inf_norm; + bool two_step_soft_constraint; + bool plan_from_ego; +}; + +#endif // OBSTACLE_AVOIDANCE_PLANNER__COMMON_STRUCTS_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/costmap_generator.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/costmap_generator.hpp new file mode 100644 index 0000000000000..8165b523ca0d2 --- /dev/null +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/costmap_generator.hpp @@ -0,0 +1,66 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 OBSTACLE_AVOIDANCE_PLANNER__COSTMAP_GENERATOR_HPP_ +#define OBSTACLE_AVOIDANCE_PLANNER__COSTMAP_GENERATOR_HPP_ + +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "tier4_autoware_utils/system/stop_watch.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_object.hpp" +#include "autoware_auto_planning_msgs/msg/path.hpp" +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "boost/optional.hpp" + +#include +#include +#include +#include + +class CostmapGenerator +{ +public: + CVMaps getMaps( + const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & path, + const std::vector & objects, + const TrajectoryParam & traj_param, std::shared_ptr debug_data_ptr); + +private: + mutable tier4_autoware_utils::StopWatch< + std::chrono::milliseconds, std::chrono::microseconds, std::chrono::steady_clock> + stop_watch_; + + cv::Mat getAreaWithObjects( + const cv::Mat & drivable_area, const cv::Mat & objects_image, + std::shared_ptr debug_data_ptr) const; + + cv::Mat getClearanceMap( + const cv::Mat & drivable_area, std::shared_ptr debug_data_ptr) const; + + cv::Mat drawObstaclesOnImage( + const bool enable_avoidance, + const std::vector & objects, + const std::vector & path_points, + const nav_msgs::msg::MapMetaData & map_info, [[maybe_unused]] const cv::Mat & drivable_area, + const cv::Mat & clearance_map, const TrajectoryParam & traj_param, + std::vector * debug_avoiding_objects, + std::shared_ptr debug_data_ptr) const; + + cv::Mat getDrivableAreaInCV( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + std::shared_ptr debug_data_ptr) const; +}; +#endif // OBSTACLE_AVOIDANCE_PLANNER__COSTMAP_GENERATOR_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/cv_utils.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/cv_utils.hpp new file mode 100644 index 0000000000000..b05a6edeb94d7 --- /dev/null +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/cv_utils.hpp @@ -0,0 +1,121 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 OBSTACLE_AVOIDANCE_PLANNER__CV_UTILS_HPP_ +#define OBSTACLE_AVOIDANCE_PLANNER__CV_UTILS_HPP_ + +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" +#include "opencv2/core.hpp" +#include "opencv2/imgproc/imgproc_c.h" +#include "opencv2/opencv.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_object.hpp" +#include "autoware_auto_planning_msgs/msg/path.hpp" +#include "geometry_msgs/msg/point32.hpp" + +#include "boost/optional/optional_fwd.hpp" + +#include +#include + +namespace util +{ +struct Footprint; +} + +struct Edges +{ + int front_idx; + int back_idx; + geometry_msgs::msg::Point extended_front; + geometry_msgs::msg::Point extended_back; + geometry_msgs::msg::Point origin; +}; + +struct PolygonPoints +{ + std::vector points_in_image; + std::vector points_in_map; +}; + +namespace cv_utils +{ +void getOccupancyGridValue( + const nav_msgs::msg::OccupancyGrid & og, const int i, const int j, unsigned char & value); + +void putOccupancyGridValue( + nav_msgs::msg::OccupancyGrid & og, const int i, const int j, const unsigned char value); +} // namespace cv_utils + +namespace cv_polygon_utils +{ +PolygonPoints getPolygonPoints( + const std::vector & points, + const nav_msgs::msg::MapMetaData & map_info); + +PolygonPoints getPolygonPoints( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info); + +PolygonPoints getPolygonPointsFromBB( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info); + +PolygonPoints getPolygonPointsFromCircle( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info); + +PolygonPoints getPolygonPointsFromPolygon( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info); + +std::vector getCVPolygon( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const PolygonPoints & polygon_points, + const std::vector & path_points, + const cv::Mat & clearance_map, const nav_msgs::msg::MapMetaData & map_info); + +std::vector getDefaultCVPolygon( + const std::vector & points_in_image); + +std::vector getExtendedCVPolygon( + const std::vector & points_in_image, + const std::vector & points_in_map, + const geometry_msgs::msg::Pose & nearest_path_point_pose, + const autoware_auto_perception_msgs::msg::PredictedObject & object, const cv::Mat & clearance_map, + const nav_msgs::msg::MapMetaData & map_info); + +boost::optional getEdges( + const std::vector & points_in_image, + const std::vector & points_in_map, + const geometry_msgs::msg::Pose & nearest_path_point_pose, + const autoware_auto_perception_msgs::msg::PredictedObject & object, const cv::Mat & clearance_map, + const nav_msgs::msg::MapMetaData & map_info); +} // namespace cv_polygon_utils + +namespace cv_drivable_area_utils +{ +bool isOutsideDrivableAreaFromRectangleFootprint( + const autoware_auto_planning_msgs::msg::TrajectoryPoint & traj_point, + const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, + const VehicleParam & vehicle_param); + +bool isOutsideDrivableAreaFromCirclesFootprint( + const autoware_auto_planning_msgs::msg::TrajectoryPoint & traj_point, + const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, + const std::vector vehicle_circle_longitudinal_offsets, + const double vehicle_circle_radius); +} // namespace cv_drivable_area_utils + +#endif // OBSTACLE_AVOIDANCE_PLANNER__CV_UTILS_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp new file mode 100644 index 0000000000000..ead547892ee24 --- /dev/null +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp @@ -0,0 +1,47 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 OBSTACLE_AVOIDANCE_PLANNER__DEBUG_VISUALIZATION_HPP_ +#define OBSTACLE_AVOIDANCE_PLANNER__DEBUG_VISUALIZATION_HPP_ + +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "opencv2/core.hpp" +#include "rclcpp/clock.hpp" +#include "tier4_autoware_utils/geometry/geometry.hpp" +#include "tier4_autoware_utils/ros/marker_helper.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_object.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory_point.hpp" +#include "geometry_msgs/msg/pose.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include +#include +#include + +using tier4_autoware_utils::createMarkerColor; +using tier4_autoware_utils::createMarkerScale; + +namespace debug_visualization +{ +visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( + const std::shared_ptr debug_data_ptr, + const std::vector & optimized_points, + const VehicleParam & vehicle_param, const bool is_showing_debug_detail); + +nav_msgs::msg::OccupancyGrid getDebugCostmap( + const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid); +} // namespace debug_visualization + +#endif // OBSTACLE_AVOIDANCE_PLANNER__DEBUG_VISUALIZATION_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/eb_path_optimizer.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/eb_path_optimizer.hpp index 7eb8d71adbacd..31c9943b4588b 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/eb_path_optimizer.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/eb_path_optimizer.hpp @@ -11,266 +11,103 @@ // 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 OBSTACLE_AVOIDANCE_PLANNER__EB_PATH_OPTIMIZER_HPP_ #define OBSTACLE_AVOIDANCE_PLANNER__EB_PATH_OPTIMIZER_HPP_ -#include -#include -#include +#include "eigen3/Eigen/Core" +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "osqp_interface/osqp_interface.hpp" +#include "tier4_autoware_utils/system/stop_watch.hpp" -#include -#include -#include -#include -#include -#include -#include +#include "autoware_auto_planning_msgs/msg/path.hpp" +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory.hpp" +#include "geometry_msgs/msg/point.hpp" +#include "geometry_msgs/msg/pose.hpp" +#include "nav_msgs/msg/map_meta_data.hpp" -#include +#include "boost/optional.hpp" #include #include -struct Bounds; -struct MPTParam; -struct ReferencePoint; - -struct Anchor -{ - geometry_msgs::msg::Pose pose; - double velocity; -}; - -struct ConstrainLines -{ - double x_coef; - double y_coef; - double lower_bound; - double upper_bound; -}; - -struct Constrain -{ - ConstrainLines top_and_bottom; - ConstrainLines left_and_right; -}; - -struct Rectangle -{ - geometry_msgs::msg::Point top_left; - geometry_msgs::msg::Point top_right; - geometry_msgs::msg::Point bottom_left; - geometry_msgs::msg::Point bottom_right; -}; - -struct OccupancyMaps -{ - std::vector> object_occupancy_map; - std::vector> road_occupancy_map; -}; - -namespace autoware::common::osqp -{ -class OSQPInterface; -} - -namespace util -{ -struct Rectangle; -struct Footprint; -} // namespace util - -class MPTOptimizer; - -struct ConstrainRectangle -{ - geometry_msgs::msg::Point top_left; - geometry_msgs::msg::Point top_right; - geometry_msgs::msg::Point bottom_left; - geometry_msgs::msg::Point bottom_right; - double velocity; - bool is_empty_driveable_area = false; - bool is_including_only_smooth_range = true; -}; - -struct ConstrainRectangles -{ - ConstrainRectangle object_constrain_rectangle; - ConstrainRectangle road_constrain_rectangle; -}; - -struct CandidatePoints -{ - std::vector fixed_points; - std::vector non_fixed_points; - int begin_path_idx; - int end_path_idx; -}; - -struct QPParam -{ - int max_iteration; - double eps_abs; - double eps_rel; - double eps_abs_for_extending; - double eps_rel_for_extending; - double eps_abs_for_visualizing; - double eps_rel_for_visualizing; -}; - -struct TrajectoryParam -{ - bool is_avoiding_unknown; - bool is_avoiding_car; - bool is_avoiding_truck; - bool is_avoiding_bus; - bool is_avoiding_bicycle; - bool is_avoiding_motorbike; - bool is_avoiding_pedestrian; - bool is_avoiding_animal; - int num_sampling_points; - int num_joint_buffer_points; - int num_joint_buffer_points_for_extending; - int num_offset_for_begin_idx; - int num_fix_points_for_extending; - double delta_arc_length_for_optimization; - double delta_arc_length_for_mpt_points; - double delta_arc_length_for_trajectory; - double delta_dist_threshold_for_closest_point; - double delta_yaw_threshold_for_closest_point; - double delta_yaw_threshold_for_straight; - double trajectory_length; - double forward_fixing_distance; - double forward_fixing_mpt_distance; - double backward_fixing_distance; - double max_avoiding_ego_velocity_ms; - double max_avoiding_objects_velocity_ms; - double center_line_width; - double acceleration_for_non_deceleration_range; - double max_dist_for_extending_end_point; -}; - -struct Trajectories -{ - std::vector smoothed_trajectory; - std::vector mpt_ref_points; - std::vector model_predictive_trajectory; - std::vector extended_trajectory; -}; - -struct ConstrainParam -{ - bool is_getting_constraints_close2path_points; - double clearance_for_fixing; - double clearance_for_straight_line; - double clearance_for_joint; - double range_for_extend_joint; - double clearance_for_only_smoothing; - double clearance_from_object_for_straight; - double min_object_clearance_for_joint; - double min_object_clearance_for_deceleration; - double clearance_from_road; - double clearance_from_object; - double extra_desired_clearance_from_road; - double max_x_constrain_search_range; - double coef_x_constrain_search_resolution; - double coef_y_constrain_search_resolution; - double keep_space_shape_x; - double keep_space_shape_y; - double max_lon_space_for_driveable_constraint; -}; - -struct VehicleParam -{ - double wheelbase; - double length; - double width; - double rear_overhang; - double front_overhang; - double max_steer_rad; - double steer_tau; -}; - -struct FOAData -{ - bool is_avoidance_possible = true; - std::vector avoiding_traj_points; - std::vector constrain_rectangles; -}; - -struct DebugData -{ - bool is_expected_to_over_drivable_area = false; - std::vector interpolated_points; - std::vector straight_points; - std::vector fixed_points; - std::vector non_fixed_points; - std::vector constrain_rectangles; - std::vector fixed_points_for_extending; - std::vector non_fixed_points_for_extending; - std::vector constrain_rectangles_for_extending; - std::vector interpolated_points_for_extending; - std::vector vehicle_footprints; - std::vector current_vehicle_footprints; - std::vector avoiding_traj_points; - std::vector smoothed_points; - std::vector avoiding_objects; - std::vector bounds; - std::vector bounds_candidate_for_base_points; - std::vector bounds_candidate_for_top_points; - std::vector bounds_candidate_for_mid_points; - std::vector fixed_mpt_points; - cv::Mat clearance_map; - cv::Mat only_object_clearance_map; - cv::Mat area_with_objects_map; - FOAData foa_data; -}; - -enum class OptMode : int { - Normal = 0, - Extending = 1, - Visualizing = 2, -}; - class EBPathOptimizer { private: - rclcpp::Clock logger_ros_clock_; + struct CandidatePoints + { + std::vector fixed_points; + std::vector non_fixed_points; + int begin_path_idx; + int end_path_idx; + }; + + struct Anchor + { + geometry_msgs::msg::Pose pose; + double velocity; + }; + + struct ConstrainRectangles + { + ConstrainRectangle object_constrain_rectangle; + ConstrainRectangle road_constrain_rectangle; + }; + + struct ConstrainLines + { + double x_coef; + double y_coef; + double lower_bound; + double upper_bound; + }; + + struct Constrain + { + ConstrainLines top_and_bottom; + ConstrainLines left_and_right; + }; + + struct Rectangle + { + geometry_msgs::msg::Point top_left; + geometry_msgs::msg::Point top_right; + geometry_msgs::msg::Point bottom_left; + geometry_msgs::msg::Point bottom_right; + }; + + struct OccupancyMaps + { + std::vector> object_occupancy_map; + std::vector> road_occupancy_map; + }; const bool is_showing_debug_info_; const double epsilon_; const QPParam qp_param_; const TrajectoryParam traj_param_; - const ConstrainParam constrain_param_; + const EBParam eb_param_; const VehicleParam vehicle_param_; Eigen::MatrixXd default_a_matrix_; - std::unique_ptr keep_space_shape_ptr_; std::unique_ptr osqp_solver_ptr_; - std::unique_ptr ex_osqp_solver_ptr_; - std::unique_ptr vis_osqp_solver_ptr_; - std::unique_ptr mpt_optimizer_ptr_; + double current_ego_vel_; - void initializeSolver(); + mutable tier4_autoware_utils::StopWatch< + std::chrono::milliseconds, std::chrono::microseconds, std::chrono::steady_clock> + stop_watch_; Eigen::MatrixXd makePMatrix(); Eigen::MatrixXd makeAMatrix(); - geometry_msgs::msg::Pose getOriginPose( - const std::vector & interpolated_points, const int interpolated_idx, - const std::vector & path_points); - Anchor getAnchor( const std::vector & interpolated_points, const int interpolated_idx, const std::vector & path_points) const; - boost::optional>> getOccupancyPoints( - const geometry_msgs::msg::Pose & origin, const cv::Mat & clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const; - Constrain getConstrainFromConstrainRectangle( const geometry_msgs::msg::Point & interpolated_point, const ConstrainRectangle & constrain_range); @@ -279,119 +116,21 @@ class EBPathOptimizer const double dx, const double dy, const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Point & opposite_point); - ConstrainRectangles getConstrainRectangles( - const Anchor & anchor, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, const nav_msgs::msg::MapMetaData & map_info) const; - ConstrainRectangle getConstrainRectangle(const Anchor & anchor, const double clearance) const; ConstrainRectangle getConstrainRectangle( - const Anchor & anchor, const int & nearest_idx, - const std::vector & interpolated_points, - const cv::Mat & clearance_map, const nav_msgs::msg::MapMetaData & map_info) const; - - ConstrainRectangle getConstrainRectangle( - const std::vector> & occupancy_map, - const std::vector> & occupancy_points, - const Anchor & anchor, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & only_objects_clearance_map) const; - - ConstrainRectangle getConstrainRectangle( - const std::vector & path_points, - const Anchor & anchor, const cv::Mat & clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const; - - ConstrainRectangle getConstrainRectangle( - const std::vector> & occupancy_points, - const util::Rectangle & util_rect, const Anchor & anchor) const; - - ConstrainRectangle getUpdatedConstrainRectangle( - const ConstrainRectangle & rectangle, const geometry_msgs::msg::Point & candidate_point, - const nav_msgs::msg::MapMetaData & map_info, const cv::Mat & only_objects_clearance_map) const; - - OccupancyMaps getOccupancyMaps( - const std::vector> & occupancy_points, - const geometry_msgs::msg::Pose & origin_pose, - const geometry_msgs::msg::Point & origin_point_in_image, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, const nav_msgs::msg::MapMetaData & map_info) const; + const Anchor & anchor, const double min_x, const double max_x, const double min_y, + const double max_y) const; int getStraightLineIdx( const std::vector & interpolated_points, - const int farthest_point_idx, const cv::Mat & only_objects_clearance, - const nav_msgs::msg::MapMetaData & map_info, + const int farthest_point_idx, std::vector & debug_detected_straight_points); - int getEndPathIdx( - const std::vector & path_points, - const int begin_path_idx, const double required_trajectory_length); - - int getEndPathIdxInsideArea( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const int begin_path_idx, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info); - - boost::optional> getPostProcessedConstrainRectangles( - const bool enable_avoidance, const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, - const std::vector & interpolated_points, - const std::vector & path_points, - const int farthest_point_idx, const int num_fixed_points, const int straight_idx, - DebugData * debug_data) const; - - boost::optional> getValidConstrainRectangles( - const std::vector & constrains, - const std::vector & only_smooth_constrains, DebugData * debug_data) const; - - boost::optional> getConstrainRectanglesClose2PathPoints( - const bool is_using_only_smooth_constrain, const bool is_using_road_constrain, - const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, DebugData * debug_data) const; - - boost::optional> getConstrainRectanglesWithinArea( - const bool is_using_only_smooth_constrain, const bool is_using_road_constrain, - const int farthest_point_idx, const int num_fixed_points, const int straight_idx, - const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, - const std::vector & interpolated_points, - const std::vector & path_points, - DebugData * debug_data) const; - - bool isPreFixIdx( - const int target_idx, const int farthest_point_idx, const int num_fixed, - const int straight_idx) const; - - bool isClose2Object( - const geometry_msgs::msg::Point & point, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & only_objects_clearance_map, const double distance_threshold) const; - - boost::optional> getConstrainRectangleVec( - const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & input_path, - const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx, const int straight_idx, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, DebugData * debug_data); - boost::optional> getConstrainRectangleVec( const autoware_auto_planning_msgs::msg::Path & path, const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx, const int straight_idx, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map); - - std::vector getConstrainRectangleVec( - const std::vector & input_path, - const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx); - - Rectangle getRelShapeRectangle( - const geometry_msgs::msg::Vector3 & vehicle_shape, - const geometry_msgs::msg::Pose & origin) const; - - Rectangle getAbsShapeRectangle( - const Rectangle & rel_shape_rectangle_points, const geometry_msgs::msg::Point & offset_point, - const geometry_msgs::msg::Pose & origin) const; + const int farthest_point_idx, const int straight_idx); std::vector getPaddedInterpolatedPoints( const std::vector & interpolated_points, const int farthest_idx); @@ -402,18 +141,12 @@ class EBPathOptimizer boost::optional> getOptimizedTrajectory( - const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & path, - const CandidatePoints & candidate_points, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, DebugData * debug_data); - - std::vector getExtendedOptimizedTrajectory( - const std::vector & path_points, - const std::vector & optimized_points, - DebugData * debug_data); + const autoware_auto_planning_msgs::msg::Path & path, const CandidatePoints & candidate_points, + std::shared_ptr debug_data_ptr); void updateConstrain( const std::vector & interpolated_points, - const std::vector & rectangle_points, const OptMode & opt_mode); + const std::vector & rectangle_points); std::vector convertOptimizedPointsToTrajectory( const std::vector optimized_points, const std::vector & constraint, @@ -422,50 +155,32 @@ class EBPathOptimizer std::vector getFixedPoints( const geometry_msgs::msg::Pose & ego_pose, const std::vector & path_points, - const std::unique_ptr & prev_optimized_points, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info); + const std::unique_ptr & prev_optimized_points); CandidatePoints getCandidatePoints( const geometry_msgs::msg::Pose & ego_pose, const std::vector & path_points, - const std::unique_ptr & prev_trajs, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info, DebugData * debug_data); - - bool isPointInsideDrivableArea( - const geometry_msgs::msg::Point & point, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info); + const std::unique_ptr & prev_trajs, std::shared_ptr debug_data_ptr); CandidatePoints getDefaultCandidatePoints( const std::vector & path_points); - std::vector solveQP(const OptMode & opt_mode); - - bool isFixingPathPoint( - const std::vector & path_points) const; + std::vector solveQP(); std::vector calculateTrajectory( const std::vector & padded_interpolated_points, const std::vector & constrain_rectangles, const int farthest_idx, - const OptMode & opt_mode); - - FOAData getFOAData( - const std::vector & rectangles, - const std::vector & interpolated_points, const int farthest_idx); + std::shared_ptr debug_data_ptr); public: EBPathOptimizer( - const bool is_showing_debug_info, const QPParam qp_param, const TrajectoryParam traj_param, - const ConstrainParam constrain_param, const VehicleParam & vehicle_param, - const MPTParam & mpt_param); - - ~EBPathOptimizer(); + const bool is_showing_debug_info, const TrajectoryParam & traj_param, const EBParam & eb_param, + const VehicleParam & vehicle_param); - boost::optional generateOptimizedTrajectory( - const bool enable_avoidance, const geometry_msgs::msg::Pose & ego_pose, - const autoware_auto_planning_msgs::msg::Path & path, - const std::unique_ptr & prev_trajs, - const std::vector & objects, - DebugData * debug_data); + boost::optional> getEBTrajectory( + const geometry_msgs::msg::Pose & ego_pose, const autoware_auto_planning_msgs::msg::Path & path, + const std::unique_ptr & prev_trajs, const double current_ego_vel, + std::shared_ptr debug_data_ptr); }; #endif // OBSTACLE_AVOIDANCE_PLANNER__EB_PATH_OPTIMIZER_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/mpt_optimizer.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/mpt_optimizer.hpp index b8e760771d6f9..f7ff8c9f6c1e6 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/mpt_optimizer.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/mpt_optimizer.hpp @@ -40,37 +40,77 @@ #ifndef OBSTACLE_AVOIDANCE_PLANNER__MPT_OPTIMIZER_HPP_ #define OBSTACLE_AVOIDANCE_PLANNER__MPT_OPTIMIZER_HPP_ +#include "eigen3/Eigen/Core" +#include "eigen3/Eigen/Sparse" +#include "interpolation/linear_interpolation.hpp" +#include "obstacle_avoidance_planner/common_structs.hpp" #include "obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp" +#include "osqp_interface/osqp_interface.hpp" +#include "tier4_autoware_utils/system/stop_watch.hpp" +#include "tier4_autoware_utils/tier4_autoware_utils.hpp" -#include +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory_point.hpp" +#include "nav_msgs/msg/map_meta_data.hpp" -#include -#include -#include - -#include +#include "boost/optional.hpp" #include #include -namespace cv -{ -class Mat; -} +enum class CollisionType { NO_COLLISION = 0, OUT_OF_SIGHT = 1, OUT_OF_ROAD = 2, OBJECT = 3 }; -namespace autoware::common::osqp +struct Bounds { -class OSQPInterface; -} - -struct DebugData; -struct TrajectoryParam; -struct QPParam; -struct ConstrainParam; -struct VehicleParam; -struct Rectangle; -struct CVMaps; -struct Trajectories; + Bounds() = default; + Bounds( + const double lower_bound_, const double upper_bound_, CollisionType lower_collision_type_, + CollisionType upper_collision_type_) + : lower_bound(lower_bound_), + upper_bound(upper_bound_), + lower_collision_type(lower_collision_type_), + upper_collision_type(upper_collision_type_) + { + } + + double lower_bound; + double upper_bound; + + CollisionType lower_collision_type; + CollisionType upper_collision_type; + + bool hasCollisionWithRightObject() const { return lower_collision_type == CollisionType::OBJECT; } + + bool hasCollisionWithLeftObject() const { return upper_collision_type == CollisionType::OBJECT; } + + bool hasCollisionWithObject() const + { + return hasCollisionWithRightObject() || hasCollisionWithLeftObject(); + } + + static Bounds lerp(Bounds prev_bounds, Bounds next_bounds, double ratio) + { + const double lower_bound = + interpolation::lerp(prev_bounds.lower_bound, next_bounds.lower_bound, ratio); + const double upper_bound = + interpolation::lerp(prev_bounds.upper_bound, next_bounds.upper_bound, ratio); + + if (ratio < 0.5) { + return Bounds{ + lower_bound, upper_bound, prev_bounds.lower_collision_type, + prev_bounds.upper_collision_type}; + } + + return Bounds{ + lower_bound, upper_bound, next_bounds.lower_collision_type, next_bounds.upper_collision_type}; + } + + void translate(const double offset) + { + lower_bound -= offset; + upper_bound -= offset; + } +}; struct ReferencePoint { @@ -78,220 +118,199 @@ struct ReferencePoint double k = 0; double v = 0; double yaw = 0; - geometry_msgs::msg::Quaternion q; double s = 0; - geometry_msgs::msg::Pose top_pose; - geometry_msgs::msg::Pose mid_pose; - double delta_yaw_from_p1 = 0; - double delta_yaw_from_p2 = 0; - boost::optional fix_state = boost::none; - Eigen::VectorXd optimized_state; + double alpha = 0.0; + Bounds bounds; + bool near_objects; + + // NOTE: fix_kinematic_state is used for two purposes + // one is fixing points around ego for stability + // second is fixing current ego pose when no velocity for planning from ego pose + boost::optional fix_kinematic_state = boost::none; + bool plan_from_ego = false; + Eigen::Vector2d optimized_kinematic_state; + double optimized_input; + + // + std::vector> beta; + VehicleBounds vehicle_bounds; + + // SequentialBoundsCandidates sequential_bounds_candidates; + std::vector vehicle_bounds_poses; // for debug visualization }; -struct Bounds +class MPTOptimizer { - struct SingleBounds +private: + struct MPTMatrix { - SingleBounds & operator=(std::vector & bounds) - { - ub = bounds[0]; - lb = bounds[1]; - return *this; - } - double ub; // left - double lb; // right - } c0, c1, c2; -}; - -struct MPTTrajs -{ - std::vector mpt; - std::vector ref_points; -}; + // Eigen::MatrixXd Aex; + Eigen::MatrixXd Bex; + Eigen::VectorXd Wex; + // Eigen::SparseMatrix Cex; + // Eigen::SparseMatrix Qex; + // Eigen::SparseMatrix Rex; + // Eigen::MatrixXd R1ex; + // Eigen::MatrixXd R2ex; + // Eigen::MatrixXd Uref_ex; + }; + + struct ValueMatrix + { + Eigen::SparseMatrix Qex; + Eigen::SparseMatrix Rex; + }; -struct MPTMatrix -{ - Eigen::MatrixXd Aex; - Eigen::MatrixXd Bex; - Eigen::MatrixXd Wex; - Eigen::MatrixXd Cex; - Eigen::MatrixXd Qex; - Eigen::MatrixXd R1ex; - Eigen::MatrixXd R2ex; - Eigen::MatrixXd Uref_ex; -}; + struct ObjectiveMatrix + { + Eigen::MatrixXd hessian; + Eigen::VectorXd gradient; + }; -struct ObjectiveMatrix -{ - Eigen::MatrixXd hessian; - std::vector gradient; -}; + struct ConstraintMatrix + { + Eigen::MatrixXd linear; + Eigen::VectorXd lower_bound; + Eigen::VectorXd upper_bound; + }; -struct ConstraintMatrix -{ - Eigen::MatrixXd linear; - std::vector lower_bound; - std::vector upper_bound; -}; + struct MPTTrajs + { + std::vector mpt; + std::vector ref_points; + }; -struct MPTParam -{ - bool is_hard_fixing_terminal_point; - int num_curvature_sampling_points; - double base_point_dist_from_base_link; - double top_point_dist_from_base_link; - double mid_point_dist_from_base_link; - double clearance_from_road; - double clearance_from_object; - double base_point_weight; - double top_point_weight; - double mid_point_weight; - double lat_error_weight; - double yaw_error_weight; - double steer_input_weight; - double steer_rate_weight; - double steer_acc_weight; - double terminal_lat_error_weight; - double terminal_yaw_error_weight; - double terminal_path_lat_error_weight; - double terminal_path_yaw_error_weight; - double zero_ff_steer_angle; -}; + const double osqp_epsilon_ = 1.0e-3; -class MPTOptimizer -{ -private: bool is_showing_debug_info_; - - std::unique_ptr osqp_solver_ptr_; - std::unique_ptr mpt_param_ptr_; - std::unique_ptr traj_param_ptr_; - std::unique_ptr qp_param_ptr_; - std::unique_ptr constraint_param_ptr_; - std::unique_ptr vehicle_param_ptr_; + TrajectoryParam traj_param_; + VehicleParam vehicle_param_; + MPTParam mpt_param_; std::unique_ptr vehicle_model_ptr_; + std::unique_ptr osqp_solver_ptr_; - std::vector convertToReferencePoints( - const std::vector & points, - const std::vector & path_points, - const std::unique_ptr & prev_mpt_points, - const geometry_msgs::msg::Pose & ego_pose, const CVMaps & maps, DebugData * debug_data) const; + geometry_msgs::msg::Pose current_ego_pose_; + double current_ego_vel_; + + int prev_mat_n = 0; + int prev_mat_m = 0; + + mutable tier4_autoware_utils::StopWatch< + std::chrono::milliseconds, std::chrono::microseconds, std::chrono::steady_clock> + stop_watch_; std::vector getReferencePoints( - const geometry_msgs::msg::Pose & origin_pose, const geometry_msgs::msg::Pose & ego_pose, const std::vector & points, - const std::vector & path_points, - const std::unique_ptr & prev_mpt_points, const CVMaps & maps, - DebugData * debug_data) const; + const std::unique_ptr & prev_mpt_points, const bool enable_avoidance, + const CVMaps & maps, std::shared_ptr debug_data_ptr) const; - std::vector getBaseReferencePoints( - const std::vector & interpolated_points, - const std::unique_ptr & prev_trajs, DebugData * debug_data) const; + void calcPlanningFromEgo(std::vector & ref_points) const; - void calcOrientation(std::vector * ref_points) const; + /* + std::vector convertToReferencePoints( + const std::vector & points, + const std::unique_ptr & prev_mpt_points, const bool enable_avoidance, + const CVMaps & maps, std::shared_ptr debug_data_ptr) const; + */ - void calcVelocity(std::vector * ref_points) const; + std::vector getFixedReferencePoints( + const std::vector & points, + const std::unique_ptr & prev_trajs) const; - void calcVelocity( - std::vector * ref_points, - const std::vector & points) const; + void calcBounds( + std::vector & ref_points, const bool enable_avoidance, const CVMaps & maps, + std::shared_ptr debug_data_ptr) const; - void calcCurvature(std::vector * ref_points) const; + void calcVehicleBounds( + std::vector & ref_points, const CVMaps & maps, + std::shared_ptr debug_data_ptr, const bool enable_avoidance) const; - void calcArcLength(std::vector * ref_points) const; + // void calcFixState( + // std::vector & ref_points, + // const std::unique_ptr & prev_trajs) const; - void calcExtraPoints(std::vector * ref_points) const; + void calcOrientation(std::vector & ref_points) const; - void calcFixPoints( - const std::unique_ptr & prev_trajs, const geometry_msgs::msg::Pose & ego_pose, - std::vector * ref_points, DebugData * debug_data) const; + void calcVelocity(std::vector & ref_points) const; - void calcInitialState( - std::vector * ref_points, const geometry_msgs::msg::Pose & origin_pose) const; + void calcVelocity( + std::vector & ref_points, + const std::vector & points) const; + + void calcCurvature(std::vector & ref_points) const; - void setOptimizedState(ReferencePoint * ref_point, const Eigen::Vector3d & optimized_state) const; + void calcArcLength(std::vector & ref_points) const; + + void calcExtraPoints( + std::vector & ref_points, + const std::unique_ptr & prev_trajs) const; /* * predict equation: Xec = Aex * x0 + Bex * Uex + Wex * cost function: J = Xex' * Qex * Xex + (Uex - Uref)' * R1ex * (Uex - Uref_ex) + Uex' * R2ex * * Uex Qex = diag([Q,Q,...]), R1ex = diag([R,R,...]) */ - boost::optional generateMPTMatrix( + MPTMatrix generateMPTMatrix( const std::vector & reference_points, - const std::vector & path_points, - const std::unique_ptr & prev_trajs) const; + std::shared_ptr debug_data_ptr) const; - void addSteerWeightR(Eigen::MatrixXd * R, const std::vector & ref_points) const; + ValueMatrix generateValueMatrix( + const std::vector & reference_points, + const std::vector & path_points, + std::shared_ptr debug_data_ptr) const; - void addSteerWeightF(Eigen::VectorXd * f) const; + void addSteerWeightR( + std::vector> & Rex_triplet_vec, + const std::vector & ref_points) const; boost::optional executeOptimization( - const bool enable_avoidance, const MPTMatrix & m, - const std::vector & ref_points, - const std::vector & path_points, - const CVMaps & maps, DebugData * debug_data); + const std::unique_ptr & prev_trajs, const bool enable_avoidance, + const MPTMatrix & mpt_mat, const ValueMatrix & obj_mat, + const std::vector & ref_points, std::shared_ptr debug_data_ptr); std::vector getMPTPoints( - std::vector & ref_points, const Eigen::VectorXd & Uex, - const MPTMatrix & mpc_matrix, - const std::vector & optimized_points) const; - - double calcLateralError( - const geometry_msgs::msg::Point & target_point, const ReferencePoint & ref_point) const; - - Eigen::VectorXd getState( - const geometry_msgs::msg::Pose & ego_pose, const ReferencePoint & nearest_ref_point) const; - - std::vector getReferenceBounds( - const bool enable_avoidance, const std::vector & ref_points, - const CVMaps & maps, DebugData * debug_data) const; + std::vector & fixed_ref_points, + std::vector & non_fixed_ref_points, const Eigen::VectorXd & Uex, + const MPTMatrix & mpt_matrix, std::shared_ptr debug_data_ptr); - boost::optional> getBound( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps) const; + std::vector getMPTFixedPoints( + const std::vector & ref_points) const; - boost::optional> getBoundCandidate( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps, - const double min_road_clearance, const double min_obj_clearance, const double rel_initial_lat, - const std::vector & rough_road_bound) const; + BoundsCandidates getBoundsCandidates( + const bool enable_avoidance, const geometry_msgs::msg::Pose & avoiding_point, + const CVMaps & maps, std::shared_ptr debug_data_ptr) const; - boost::optional> getRoughBound( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps) const; + CollisionType getCollisionType( + const CVMaps & maps, const bool enable_avoidance, + const geometry_msgs::msg::Pose & avoiding_point, const double traversed_dist, + const double bound_angle) const; - double getTraversedDistance( - const bool enable_avoidance, const ReferencePoint & ref_point, const double traverse_angle, - const double initial_value, const CVMaps & maps, const double min_road_clearance, - const double min_obj_clearance, const bool search_expanding_side = false) const; - - boost::optional getValidLatError( - const bool enable_avoidance, const ReferencePoint & ref_point, const double initial_value, - const CVMaps & maps, const double min_road_clearance, const double min_obj_clearance, - const std::vector & rough_road_bound, const bool search_expanding_side = false) const; - - // TODO(unknown): refactor replace all relevant funcs boost::optional getClearance( const cv::Mat & clearance_map, const geometry_msgs::msg::Point & map_point, const nav_msgs::msg::MapMetaData & map_info) const; - ObjectiveMatrix getObjectiveMatrix(const Eigen::VectorXd & x0, const MPTMatrix & m) const; + ObjectiveMatrix getObjectiveMatrix( + const MPTMatrix & mpt_mat, const ValueMatrix & obj_mat, + [[maybe_unused]] const std::vector & ref_points, + std::shared_ptr debug_data_ptr) const; ConstraintMatrix getConstraintMatrix( - const bool enable_avoidance, const Eigen::VectorXd & x0, const MPTMatrix & m, - const CVMaps & maps, const std::vector & ref_points, - const std::vector & path_points, - DebugData * debug_data) const; + const bool enable_avoidance, const MPTMatrix & mpt_mat, + const std::vector & ref_points, + std::shared_ptr debug_data_ptr) const; public: MPTOptimizer( - const bool is_showing_debug_info, const QPParam & qp_param, const TrajectoryParam & traj_param, - const ConstrainParam & constrain_param, const VehicleParam & vehicle_param, - const MPTParam & mpt_param); + const bool is_showing_debug_info, const TrajectoryParam & traj_param, + const VehicleParam & vehicle_param, const MPTParam & mpt_param); boost::optional getModelPredictiveTrajectory( const bool enable_avoidance, const std::vector & smoothed_points, const std::vector & path_points, const std::unique_ptr & prev_trajs, const CVMaps & maps, - const geometry_msgs::msg::Pose & ego_pose, DebugData * debug_data); + const geometry_msgs::msg::Pose & current_ego_pose, const double current_ego_vel, + std::shared_ptr debug_data_ptr); }; #endif // OBSTACLE_AVOIDANCE_PLANNER__MPT_OPTIMIZER_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp index e184847cd7e7f..56f4e437853f3 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp @@ -14,207 +14,228 @@ #ifndef OBSTACLE_AVOIDANCE_PLANNER__NODE_HPP_ #define OBSTACLE_AVOIDANCE_PLANNER__NODE_HPP_ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "obstacle_avoidance_planner/costmap_generator.hpp" +#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" +#include "obstacle_avoidance_planner/mpt_optimizer.hpp" +#include "opencv2/core.hpp" +#include "rclcpp/clock.hpp" +#include "rclcpp/rclcpp.hpp" +#include "tier4_autoware_utils/ros/self_pose_listener.hpp" +#include "tier4_autoware_utils/system/stop_watch.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_objects.hpp" +#include "autoware_auto_planning_msgs/msg/path.hpp" +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory_point.hpp" +#include "geometry_msgs/msg/point.hpp" +#include "geometry_msgs/msg/pose.hpp" +#include "geometry_msgs/msg/twist.hpp" +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/map_meta_data.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" +#include "nav_msgs/msg/odometry.hpp" +#include "tier4_debug_msgs/msg/string_stamped.hpp" +#include "tier4_planning_msgs/msg/enable_avoidance.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "boost/optional.hpp" #include -#include #include -namespace ros +namespace { -class Time; -} - -namespace cv +template +double lerpTwistX( + const T & points, const geometry_msgs::msg::Point & target_pos, const size_t closest_seg_idx) { -class Mat; + constexpr double epsilon = 1e-6; + + const double closest_to_target_dist = + tier4_autoware_utils::calcSignedArcLength(points, closest_seg_idx, target_pos); + const double seg_dist = + tier4_autoware_utils::calcSignedArcLength(points, closest_seg_idx, closest_seg_idx + 1); + + const double closest_vel = points[closest_seg_idx].longitudinal_velocity_mps; + const double next_vel = points[closest_seg_idx + 1].longitudinal_velocity_mps; + + return std::abs(seg_dist) < epsilon + ? next_vel + : interpolation::lerp(closest_vel, next_vel, closest_to_target_dist / seg_dist); } -namespace tf2_ros +template +double lerpPoseZ( + const T & points, const geometry_msgs::msg::Point & target_pos, const size_t closest_seg_idx) { -class Buffer; -class TransformListener; -} // namespace tf2_ros + constexpr double epsilon = 1e-6; -class EBPathOptimizer; + const double closest_to_target_dist = + tier4_autoware_utils::calcSignedArcLength(points, closest_seg_idx, target_pos); + const double seg_dist = + tier4_autoware_utils::calcSignedArcLength(points, closest_seg_idx, closest_seg_idx + 1); -struct QPParam; -struct TrajectoryParam; -struct ConstrainParam; -struct VehicleParam; -struct MPTParam; -struct DebugData; -struct Trajectories; + const double closest_z = points[closest_seg_idx].pose.position.z; + const double next_z = points[closest_seg_idx + 1].pose.position.z; + + return std::abs(seg_dist) < epsilon + ? next_z + : interpolation::lerp(closest_z, next_z, closest_to_target_dist / seg_dist); +} +} // namespace class ObstacleAvoidancePlanner : public rclcpp::Node { private: OnSetParametersCallbackHandle::SharedPtr set_param_res_; + rclcpp::Clock logger_ros_clock_; + int eb_solved_count_; + bool is_publishing_debug_visualization_marker_; bool is_publishing_area_with_objects_; + bool is_publishing_object_clearance_map_; bool is_publishing_clearance_map_; bool is_showing_debug_info_; - bool is_using_vehicle_config_; + bool is_showing_calculation_time_; bool is_stopping_if_outside_drivable_area_; bool enable_avoidance_; - const int min_num_points_for_getting_yaw_; - std::mutex mutex_; - - // params outside logic - double min_delta_dist_for_replan_; - double min_delta_time_sec_for_replan_; - double max_dist_for_extending_end_point_; - double distance_for_path_shape_change_detection_; + bool enable_pre_smoothing_; + bool skip_optimization_; + bool reset_prev_optimization_; + + // vehicle circles info for drivability check + bool use_vehicle_circles_for_drivability_; + bool use_manual_vehicle_circles_for_drivability_; + int vehicle_circle_constraints_num_for_drivability_; + int vehicle_circle_radius_num_for_drivability_; + double vehicle_circle_radius_ratio_for_drivability_; + double vehicle_circle_radius_for_drivability_; + std::vector vehicle_circle_longitudinal_offsets_for_drivability_; + + // vehicle circles info for for mpt constraints + bool use_manual_vehicle_circles_for_mpt_; + int vehicle_circle_constraints_num_for_mpt_; + int vehicle_circle_radius_num_for_mpt_; + double vehicle_circle_radius_ratio_for_mpt_; + + // params for replan + double max_path_shape_change_dist_for_replan_; + double max_ego_moving_dist_for_replan_; + double max_delta_time_sec_for_replan_; // logic + std::unique_ptr costmap_generator_ptr_; std::unique_ptr eb_path_optimizer_ptr_; + std::unique_ptr mpt_optimizer_ptr_; // params - std::unique_ptr qp_param_; - std::unique_ptr traj_param_; - std::unique_ptr constrain_param_; - std::unique_ptr vehicle_param_; - std::unique_ptr mpt_param_; - - std::unique_ptr current_ego_pose_ptr_; + TrajectoryParam traj_param_; + EBParam eb_param_; + VehicleParam vehicle_param_; + MPTParam mpt_param_; + int mpt_visualize_sampling_num_; + + // debug + mutable std::shared_ptr debug_data_ptr_; + mutable tier4_autoware_utils::StopWatch< + std::chrono::milliseconds, std::chrono::microseconds, std::chrono::steady_clock> + stop_watch_; + + geometry_msgs::msg::Pose current_ego_pose_; std::unique_ptr current_twist_ptr_; std::unique_ptr prev_ego_pose_ptr_; - std::unique_ptr prev_trajectories_ptr_; + std::unique_ptr prev_optimal_trajs_ptr_; std::unique_ptr> prev_path_points_ptr_; - std::unique_ptr in_objects_ptr_; + std::unique_ptr objects_ptr_; - // TF - std::unique_ptr tf_buffer_ptr_; - std::unique_ptr tf_listener_ptr_; - std::unique_ptr prev_replanned_time_ptr_; + std::unique_ptr latest_replanned_time_ptr_; + tier4_autoware_utils::SelfPoseListener self_pose_listener_{this}; // ROS - rclcpp::Publisher::SharedPtr trajectory_pub_; - rclcpp::Publisher::SharedPtr avoiding_traj_pub_; + rclcpp::Publisher::SharedPtr traj_pub_; + rclcpp::Publisher::SharedPtr + debug_extended_fixed_traj_pub_; rclcpp::Publisher::SharedPtr - debug_smoothed_points_pub_; - rclcpp::Publisher::SharedPtr - is_avoidance_possible_pub_; + debug_extended_non_fixed_traj_pub_; + rclcpp::Publisher::SharedPtr debug_eb_traj_pub_; + rclcpp::Publisher::SharedPtr + debug_mpt_fixed_traj_pub_; + rclcpp::Publisher::SharedPtr + debug_mpt_ref_traj_pub_; + rclcpp::Publisher::SharedPtr debug_mpt_traj_pub_; + rclcpp::Publisher::SharedPtr debug_markers_pub_; rclcpp::Publisher::SharedPtr debug_clearance_map_pub_; rclcpp::Publisher::SharedPtr debug_object_clearance_map_pub_; rclcpp::Publisher::SharedPtr debug_area_with_objects_pub_; + rclcpp::Publisher::SharedPtr debug_msg_pub_; + rclcpp::Subscription::SharedPtr path_sub_; rclcpp::Subscription::SharedPtr odom_sub_; rclcpp::Subscription::SharedPtr objects_sub_; rclcpp::Subscription::SharedPtr is_avoidance_sub_; - // callback functions - void pathCallback(const autoware_auto_planning_msgs::msg::Path::SharedPtr); + // param callback function + rcl_interfaces::msg::SetParametersResult paramCallback( + const std::vector & parameters); + + // subscriber callback functions void odomCallback(const nav_msgs::msg::Odometry::SharedPtr); void objectsCallback(const autoware_auto_perception_msgs::msg::PredictedObjects::SharedPtr); void enableAvoidanceCallback(const tier4_planning_msgs::msg::EnableAvoidance::SharedPtr); + void pathCallback(const autoware_auto_planning_msgs::msg::Path::SharedPtr); - void initialize(); - - // generate fine trajectory - std::vector generatePostProcessedTrajectory( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const std::vector & merged_optimized_points) - const; - - bool needReplan( - const geometry_msgs::msg::Pose & ego_pose, - const std::unique_ptr & prev_ego_pose, - const std::vector & path_points, - const std::unique_ptr & previous_replanned_time, - const std::unique_ptr> & - prev_path_points, - std::unique_ptr & prev_traj_points); + // functions + void resetPlanning(); + void resetPrevOptimization(); std::vector generateOptimizedTrajectory( - const geometry_msgs::msg::Pose & ego_pose, const autoware_auto_planning_msgs::msg::Path & input_path); - std::unique_ptr getCurrentEgoPose(); - - bool isPathShapeChanged( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const std::unique_ptr> & - prev_path_points); + bool checkReplan(const std::vector & path_points); - autoware_auto_planning_msgs::msg::Trajectory generateTrajectory( - const autoware_auto_planning_msgs::msg::Path & in_path); + Trajectories optimizeTrajectory( + const autoware_auto_planning_msgs::msg::Path & path, const CVMaps & cv_maps); - std::vector convertPointsToTrajectory( - const std::vector & path_points, - const std::vector & trajectory_points) const; + Trajectories getPrevTrajs( + const std::vector & path_points) const; - void publishingDebugData( - const DebugData & debug_data, const autoware_auto_planning_msgs::msg::Path & path, - const std::vector & traj_points, - const VehicleParam & vehicle_param); + void insertZeroVelocityOutsideDrivableArea( + std::vector & traj_points, + const CVMaps & cv_maps); - int calculateNonDecelerationRange( - const std::vector & traj_points, - const geometry_msgs::msg::Pose & ego_pose, const geometry_msgs::msg::Twist & ego_twist) const; + void publishDebugDataInOptimization( + const autoware_auto_planning_msgs::msg::Path & path, + const std::vector & traj_points); - Trajectories getTrajectoryInsideArea( - const Trajectories & trajs, + Trajectories makePrevTrajectories( const std::vector & path_points, - const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, - DebugData * debug_data) const; + const Trajectories & trajs); - boost::optional calcTrajectoryInsideArea( - const Trajectories & trajs, + std::vector generatePostProcessedTrajectory( const std::vector & path_points, - const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, - DebugData * debug_data, const bool is_prev_traj = false) const; - - Trajectories getPrevTrajs( - const std::vector & path_points) const; - - std::vector getPrevTrajectory( - const std::vector & path_points) const; + const std::vector & merged_optimized_points); - Trajectories makePrevTrajectories( - const geometry_msgs::msg::Pose & ego_pose, + std::vector getExtendedTrajectory( const std::vector & path_points, - const Trajectories & trajs) const; + const std::vector & optimized_points); - Trajectories getBaseTrajectory( + std::vector generateFineTrajectoryPoints( const std::vector & path_points, - const Trajectories & current_trajs) const; + const std::vector & traj_points) const; - boost::optional getStopIdx( + std::vector alignVelocity( + const std::vector & fine_traj_points, const std::vector & path_points, - const Trajectories & trajs, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & road_clearance_map, DebugData * debug_data) const; - - void declareObstacleAvoidancePlannerParameters(); + const std::vector & traj_points) const; - rcl_interfaces::msg::SetParametersResult paramCallback( - const std::vector & parameters); + void publishDebugDataInMain(const autoware_auto_planning_msgs::msg::Path & path) const; public: explicit ObstacleAvoidancePlanner(const rclcpp::NodeOptions & node_options); - ~ObstacleAvoidancePlanner(); }; #endif // OBSTACLE_AVOIDANCE_PLANNER__NODE_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/util.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/util.hpp deleted file mode 100644 index 1ad7a77cf416a..0000000000000 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/util.hpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2020 Tier IV, Inc. -// -// 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 OBSTACLE_AVOIDANCE_PLANNER__UTIL_HPP_ -#define OBSTACLE_AVOIDANCE_PLANNER__UTIL_HPP_ - -#include -#include - -#include -#include - -#include - -#include -#include - -struct VehicleParam; -struct ReferencePoint; -struct Trajectories; -struct TrajectoryParam; - -namespace util -{ -template -geometry_msgs::msg::Point transformToRelativeCoordinate2D( - const T & point, const geometry_msgs::msg::Pose & origin); - -geometry_msgs::msg::Point transformToAbsoluteCoordinate2D( - const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Pose & origin); - -double calculate2DDistance( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b); - -double calculateSquaredDistance( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b); - -double getYawFromPoints( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root); - -double normalizeRadian(const double angle); - -geometry_msgs::msg::Quaternion getQuaternionFromPoints( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root); - -geometry_msgs::msg::Quaternion getQuaternionFromYaw(const double yaw); - -template -geometry_msgs::msg::Point transformMapToImage( - const T & map_point, const nav_msgs::msg::MapMetaData & occupancy_grid_info); - -boost::optional transformMapToOptionalImage( - const geometry_msgs::msg::Point & map_point, - const nav_msgs::msg::MapMetaData & occupancy_grid_info); - -bool transformMapToImage( - const geometry_msgs::msg::Point & map_point, - const nav_msgs::msg::MapMetaData & occupancy_grid_info, geometry_msgs::msg::Point & image_point); - -bool interpolate2DPoints( - const std::vector & x, const std::vector & y, const double resolution, - std::vector & interpolated_points); - -std::vector getInterpolatedPoints( - const std::vector & first_points, - const std::vector & second_points, const double delta_arc_length); - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length); - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length); - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length); - -template -std::vector getInterpolatedPoints( - const T & points, const double delta_arc_length); - -template -int getNearestIdx( - const T & points, const geometry_msgs::msg::Pose & pose, const int default_idx, - const double delta_yaw_threshold); - -int getNearestIdx( - const std::vector & points, const geometry_msgs::msg::Pose & pose, - const int default_idx, const double delta_yaw_threshold); - -int getNearestIdxOverPoint( - const std::vector & points, - const geometry_msgs::msg::Pose & pose, const int default_idx, const double delta_yaw_threshold); - -template -int getNearestIdx(const T & points, const geometry_msgs::msg::Point & point, const int default_idx); - -int getNearestIdx( - const std::vector & points, const geometry_msgs::msg::Point & point); - -template -int getNearestIdx(const T & points, const geometry_msgs::msg::Point & point); - -int getNearestIdx( - const std::vector & points, const double target_s, const int begin_idx); - -template -int getNearestPointIdx(const T & points, const geometry_msgs::msg::Point & point); - -std::vector convertPathToTrajectory( - const std::vector & path_points); - -std::vector -convertPointsToTrajectoryPointsWithYaw(const std::vector & points); - -std::vector fillTrajectoryWithVelocity( - const std::vector & traj_points, - const double velocity); - -template -std::vector alignVelocityWithPoints( - const std::vector & traj_points, - const T & points, const int zero_velocity_traj_idx, const int max_skip_comparison_idx); - -struct Rectangle -{ - int min_x_idx = 0; - int min_y_idx = 0; - int max_x_idx = 0; - int max_y_idx = 0; - int area = 0; -}; - -std::vector> getHistogramTable(const std::vector> & input); - -Rectangle getLargestRectangleInRow( - const std::vector & histo, const int current_row, const int row_size); - -Rectangle getLargestRectangle(const std::vector> & input); - -boost::optional getLastExtendedPoint( - const autoware_auto_planning_msgs::msg::PathPoint & path_point, - const geometry_msgs::msg::Pose & pose, const double delta_yaw_threshold, - const double delta_dist_threshold); - -boost::optional getLastExtendedTrajPoint( - const autoware_auto_planning_msgs::msg::PathPoint & path_point, - const geometry_msgs::msg::Pose & pose, const double delta_yaw_threshold, - const double delta_dist_threshold); - -struct Footprint -{ - geometry_msgs::msg::Point p; - geometry_msgs::msg::Point top_left; - geometry_msgs::msg::Point top_right; - geometry_msgs::msg::Point bottom_left; - geometry_msgs::msg::Point bottom_right; -}; - -std::vector getVehicleFootprints( - const std::vector & optimized_points, - const VehicleParam & vehicle_param); - -std::vector calcEuclidDist(const std::vector & x, const std::vector & y); - -bool hasValidNearestPointFromEgo( - const geometry_msgs::msg::Pose & ego_pose, const Trajectories & trajs, - const TrajectoryParam & traj_param); - -std::vector concatTraj( - const Trajectories & trajs); - -int getZeroVelocityIdx( - const bool is_showing_debug_info, const std::vector & fine_points, - const std::vector & path_points, - const std::unique_ptr & opt_trajs, const TrajectoryParam & traj_param); - -template -int getZeroVelocityIdxFromPoints( - const T & points, const std::vector & fine_points, - const int /* default_idx */, const TrajectoryParam & traj_param); - -template -double getArcLength(const T & points, const int initial_idx = 0); - -double getArcLength( - const std::vector & points, const int initial_idx = 0); - -void logOSQPSolutionStatus(const int solution_status); - -} // namespace util - -#endif // OBSTACLE_AVOIDANCE_PLANNER__UTIL_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/utils.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/utils.hpp new file mode 100644 index 0000000000000..602c5db22e0ac --- /dev/null +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/utils.hpp @@ -0,0 +1,336 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 OBSTACLE_AVOIDANCE_PLANNER__UTILS_HPP_ +#define OBSTACLE_AVOIDANCE_PLANNER__UTILS_HPP_ + +#include "eigen3/Eigen/Core" +#include "interpolation/linear_interpolation.hpp" +#include "interpolation/spline_interpolation.hpp" +#include "obstacle_avoidance_planner/common_structs.hpp" +#include "tier4_autoware_utils/trajectory/trajectory.hpp" + +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory.hpp" + +#include "boost/optional/optional_fwd.hpp" + +#include +#include +#include + +struct ReferencePoint; + +namespace tier4_autoware_utils +{ +template <> +geometry_msgs::msg::Point getPoint(const ReferencePoint & p); + +template <> +geometry_msgs::msg::Pose getPose(const ReferencePoint & p); +} // namespace tier4_autoware_utils + +namespace geometry_utils +{ +template +geometry_msgs::msg::Point transformToRelativeCoordinate2D( + const T & point, const geometry_msgs::msg::Pose & origin) +{ + // NOTE: implement transformation without defining yaw variable + // but directly sin/cos of yaw for fast calculation + const auto & q = origin.orientation; + const double cos_yaw = 1 - 2 * q.z * q.z; + const double sin_yaw = 2 * q.w * q.z; + + geometry_msgs::msg::Point relative_p; + const double tmp_x = point.x - origin.position.x; + const double tmp_y = point.y - origin.position.y; + relative_p.x = tmp_x * cos_yaw + tmp_y * sin_yaw; + relative_p.y = -tmp_x * sin_yaw + tmp_y * cos_yaw; + relative_p.z = point.z; + + return relative_p; +} + +geometry_msgs::msg::Point transformToAbsoluteCoordinate2D( + const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Pose & origin); + +geometry_msgs::msg::Quaternion getQuaternionFromPoints( + const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root); + +geometry_msgs::msg::Quaternion getQuaternionFromPoints( + const geometry_msgs::msg::Point & p1, const geometry_msgs::msg::Point & p2, + const geometry_msgs::msg::Point & p3, const geometry_msgs::msg::Point & p4); + +template +geometry_msgs::msg::Point transformMapToImage( + const T & map_point, const nav_msgs::msg::MapMetaData & occupancy_grid_info) +{ + geometry_msgs::msg::Point relative_p = + transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); + double resolution = occupancy_grid_info.resolution; + double map_y_height = occupancy_grid_info.height; + double map_x_width = occupancy_grid_info.width; + double map_x_in_image_resolution = relative_p.x / resolution; + double map_y_in_image_resolution = relative_p.y / resolution; + geometry_msgs::msg::Point image_point; + image_point.x = map_y_height - map_y_in_image_resolution; + image_point.y = map_x_width - map_x_in_image_resolution; + return image_point; +} + +boost::optional transformMapToOptionalImage( + const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & occupancy_grid_info); + +bool transformMapToImage( + const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & occupancy_grid_info, geometry_msgs::msg::Point & image_point); +} // namespace geometry_utils + +namespace interpolation_utils +{ +std::vector interpolate2DPoints( + const std::vector & base_x, const std::vector & base_y, const double resolution, + const double offset); + +std::vector interpolateConnected2DPoints( + const std::vector & base_x, const std::vector & base_y, const double resolution, + const double begin_yaw, const double end_yaw); + +std::vector interpolate2DTrajectoryPoints( + const std::vector & base_x, const std::vector & base_y, + const std::vector & base_yaw, const double resolution); + +template +std::vector getInterpolatedPoints( + const T & points, const double delta_arc_length, const double offset = 0) +{ + constexpr double epsilon = 1e-6; + std::vector tmp_x; + std::vector tmp_y; + for (size_t i = 0; i < points.size(); i++) { + const auto & current_point = tier4_autoware_utils::getPoint(points.at(i)); + if (i > 0) { + const auto & prev_point = tier4_autoware_utils::getPoint(points.at(i - 1)); + if ( + std::fabs(current_point.x - prev_point.x) < epsilon && + std::fabs(current_point.y - prev_point.y) < epsilon) { + continue; + } + } + tmp_x.push_back(current_point.x); + tmp_y.push_back(current_point.y); + } + + return interpolation_utils::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, offset); +} + +std::vector getInterpolatedTrajectoryPoints( + const std::vector & points, + const double delta_arc_length); + +std::vector getConnectedInterpolatedPoints( + const std::vector & points, + const double delta_arc_length, const double begin_yaw, const double end_yaw); +} // namespace interpolation_utils + +namespace points_utils +{ +template +size_t findForwardIndex(const T & points, const size_t begin_idx, const double forward_length) +{ + double sum_length = 0.0; + for (size_t i = begin_idx; i < points.size() - 1; ++i) { + sum_length += tier4_autoware_utils::calcDistance2d(points.at(i), points.at(i + 1)); + if (sum_length > forward_length) { + return i; + } + } + return points.size() - 1; +} + +template +T clipForwardPoints(const T & points, const size_t begin_idx, const double forward_length) +{ + if (points.empty()) { + return T{}; + } + + const size_t end_idx = findForwardIndex(points, begin_idx, forward_length); + return T{points.begin() + begin_idx, points.begin() + end_idx}; +} + +template +T clipBackwardPoints( + const T & points, const size_t target_idx, const double backward_length, + const double delta_length) +{ + if (points.empty()) { + return T{}; + } + + const int begin_idx = + std::max(0, static_cast(target_idx) - static_cast(backward_length / delta_length)); + return T{points.begin() + begin_idx, points.end()}; +} + +template +T clipBackwardPoints( + const T & points, const geometry_msgs::msg::Point pos, const double backward_length, + const double delta_length) +{ + if (points.empty()) { + return T{}; + } + + const size_t target_idx = tier4_autoware_utils::findNearestIndex(points, pos); + + const int begin_idx = + std::max(0, static_cast(target_idx) - static_cast(backward_length / delta_length)); + return T{points.begin() + begin_idx, points.end()}; +} + +// NOTE: acceleration is not converted +template +std::vector convertToPoints(const std::vector & points) +{ + std::vector geom_points; + for (const auto & point : points) { + geom_points.push_back(tier4_autoware_utils::getPoint(point)); + } + return geom_points; +} + +template +autoware_auto_planning_msgs::msg::TrajectoryPoint convertToTrajectoryPoint(const T & point) +{ + autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; + traj_point.pose = tier4_autoware_utils::getPose(point); + traj_point.longitudinal_velocity_mps = point.longitudinal_velocity_mps; + traj_point.lateral_velocity_mps = point.lateral_velocity_mps; + traj_point.heading_rate_rps = point.heading_rate_rps; + return traj_point; +} + +template <> +inline autoware_auto_planning_msgs::msg::TrajectoryPoint convertToTrajectoryPoint( + const ReferencePoint & point) +{ + autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; + traj_point.pose = tier4_autoware_utils::getPose(point); + return traj_point; +} + +// functions to convert to another type of points +template +std::vector convertToTrajectoryPoints( + const std::vector & points) +{ + std::vector traj_points; + for (const auto & point : points) { + const auto traj_point = convertToTrajectoryPoint(point); + traj_points.push_back(traj_point); + } + return traj_points; +} + +template +ReferencePoint convertToReferencePoint(const T & point); + +template +std::vector convertToReferencePoints(const std::vector & points) +{ + std::vector ref_points; + for (const auto & point : points) { + const auto ref_point = convertToReferencePoint(point); + ref_points.push_back(ref_point); + } + return ref_points; +} + +std::vector convertToPosesWithYawEstimation( + const std::vector points); + +std::vector concatTrajectory( + const std::vector & traj_points, + const std::vector & extended_traj_points); + +void compensateLastPose( + const autoware_auto_planning_msgs::msg::PathPoint & last_path_point, + std::vector & traj_points, + const double delta_dist_threshold, const double delta_yaw_threshold); + +template +std::vector calcCurvature(const T & points, const size_t num_sampling_points) +{ + std::vector res(points.size()); + const size_t num_points = static_cast(points.size()); + + /* calculate curvature by circle fitting from three points */ + geometry_msgs::msg::Point p1, p2, p3; + size_t max_smoothing_num = static_cast(std::floor(0.5 * (num_points - 1))); + size_t L = std::min(num_sampling_points, max_smoothing_num); + for (size_t i = L; i < num_points - L; ++i) { + p1 = tier4_autoware_utils::getPoint(points.at(i - L)); + p2 = tier4_autoware_utils::getPoint(points.at(i)); + p3 = tier4_autoware_utils::getPoint(points.at(i + L)); + double den = std::max( + tier4_autoware_utils::calcDistance2d(p1, p2) * tier4_autoware_utils::calcDistance2d(p2, p3) * + tier4_autoware_utils::calcDistance2d(p3, p1), + 0.0001); + const double curvature = + 2.0 * ((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) / den; + res.at(i) = curvature; + } + + /* first and last curvature is copied from next value */ + for (size_t i = 0; i < std::min(L, num_points); ++i) { + res.at(i) = res.at(std::min(L, num_points - 1)); + res.at(num_points - i - 1) = + res.at(std::max(static_cast(num_points) - static_cast(L) - 1, 0)); + } + return res; +} + +int getNearestIdx( + const std::vector & points, const double target_s, const int begin_idx); + +template +bool isNearLastPathPoint( + const T & ref_point, const std::vector & path_points, + const double delta_dist_threshold, const double delta_yaw_threshold) +{ + const geometry_msgs::msg::Pose last_ref_pose = tier4_autoware_utils::getPose(ref_point); + + if ( + tier4_autoware_utils::calcDistance2d(last_ref_pose, path_points.back().pose) > + delta_dist_threshold) { + return false; + } + if ( + std::fabs(tier4_autoware_utils::calcYawDeviation(last_ref_pose, path_points.back().pose)) > + delta_yaw_threshold) { + return false; + } + return true; +} +} // namespace points_utils + +namespace utils +{ +void logOSQPSolutionStatus(const int solution_status); +} // namespace utils + +#endif // OBSTACLE_AVOIDANCE_PLANNER__UTILS_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp index a7e623bc099c3..6cce41c70f227 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp @@ -48,10 +48,11 @@ #ifndef OBSTACLE_AVOIDANCE_PLANNER__VEHICLE_MODEL__VEHICLE_MODEL_BICYCLE_KINEMATICS_HPP_ #define OBSTACLE_AVOIDANCE_PLANNER__VEHICLE_MODEL__VEHICLE_MODEL_BICYCLE_KINEMATICS_HPP_ +#include "eigen3/Eigen/Core" +#include "eigen3/Eigen/LU" #include "obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp" -#include -#include +#include /** * @class vehicle model class of bicycle kinematics @@ -64,9 +65,8 @@ class KinematicsBicycleModel : public VehicleModelInterface * @brief constructor with parameter initialization * @param [in] wheelbase wheelbase length [m] * @param [in] steer_lim steering angle limit [rad] - * @param [in] steer_tau steering time constant for 1d-model */ - KinematicsBicycleModel(const double wheelbase, const double steer_lim, const double steer_tau); + KinematicsBicycleModel(const double wheel_base, const double steer_limit); /** * @brief destructor @@ -74,26 +74,31 @@ class KinematicsBicycleModel : public VehicleModelInterface virtual ~KinematicsBicycleModel() = default; /** - * @brief calculate discrete model matrix of x_k+1 = Ad * xk + Bd * uk + Wd, yk = Cd * xk + * @brief calculate discrete kinematics equation matrix of x_k+1 = Ad * x_k + Bd * uk + Wd * @param [out] Ad coefficient matrix * @param [out] Bd coefficient matrix - * @param [out] Cd coefficient matrix * @param [out] Wd coefficient matrix - * @param [in] dt Discretization arc length + * @param [in] ds discretization arc length + */ + void calculateStateEquationMatrix( + Eigen::MatrixXd & Ad, Eigen::MatrixXd & Bd, Eigen::MatrixXd & Wd, const double ds) override; + + /** + * @brief calculate discrete observation matrix of y_k = Cd * x_k + * @param [out] Cd coefficient matrix */ - void calculateDiscreteMatrix( - Eigen::MatrixXd * Ad, Eigen::MatrixXd * Bd, Eigen::MatrixXd * Cd, Eigen::MatrixXd * Wd, - const double ds) override; + void calculateObservationMatrix(Eigen::MatrixXd & Cd) override; + + /** + * @brief calculate discrete observation matrix of y_k = Cd * x_k + * @param [out] Cd_vec sparse matrix information of coefficient matrix + */ + void calculateObservationSparseMatrix(std::vector> & Cd_vec) override; /** * @brief calculate reference input * @param [out] reference input */ void calculateReferenceInput(Eigen::MatrixXd * Uref) override; - -private: - double wheelbase_; //!< @brief wheelbase length [m] - double steer_lim_; //!< @brief steering angle limit [rad] - double steer_tau_; //!< @brief steering time constant for 1d-model }; #endif // OBSTACLE_AVOIDANCE_PLANNER__VEHICLE_MODEL__VEHICLE_MODEL_BICYCLE_KINEMATICS_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp index 52f99e46aacc6..55de7dadc3d03 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp @@ -22,7 +22,10 @@ #ifndef OBSTACLE_AVOIDANCE_PLANNER__VEHICLE_MODEL__VEHICLE_MODEL_INTERFACE_HPP_ #define OBSTACLE_AVOIDANCE_PLANNER__VEHICLE_MODEL__VEHICLE_MODEL_INTERFACE_HPP_ -#include +#include "eigen3/Eigen/Core" +#include "eigen3/Eigen/Sparse" + +#include /** * @class vehicle model class @@ -31,20 +34,23 @@ class VehicleModelInterface { protected: - const int dim_x_; //!< @brief dimension of state x - const int dim_u_; //!< @brief dimension of input u - const int dim_y_; //!< @brief dimension of output y - double velocity_; //!< @brief vehicle velocity - double curvature_; //!< @brief curvature on the linearized point on path + const int dim_x_; // !< @brief dimension of kinematics x + const int dim_u_; // !< @brief dimension of input u + const int dim_y_; // !< @brief dimension of output y + double velocity_; // !< @brief vehicle velocity + double curvature_; // !< @brief curvature on the linearized point on path + double wheel_base_; // !< @brief wheel base of vehicle + double steer_limit_; // !< @brief vehicle velocity + double center_offset_from_base_; // !< @brief length from base lin to optimization center [m] public: /** * @brief constructor - * @param [in] dim_x dimension of state x + * @param [in] dim_x dimension of kinematics x * @param [in] dim_u dimension of input u * @param [in] dim_y dimension of output y */ - VehicleModelInterface(int dim_x, int dim_u, int dim_y); + VehicleModelInterface(int dim_x, int dim_u, int dim_y, double wheel_base, double steer_limit); /** * @brief destructor @@ -52,8 +58,8 @@ class VehicleModelInterface virtual ~VehicleModelInterface() = default; /** - * @brief get state x dimension - * @return state dimension + * @brief get kinematics x dimension + * @return kinematics dimension */ int getDimX(); @@ -69,6 +75,8 @@ class VehicleModelInterface */ int getDimY(); + void updateCenterOffset(const double center_offset_from_base); + /** * @brief set curvature * @param [in] curvature curvature on the linearized point on path @@ -76,16 +84,26 @@ class VehicleModelInterface void setCurvature(const double curvature); /** - * @brief calculate discrete model matrix of x_k+1 = Ad * xk + Bd * uk + Wd, yk = Cd * xk + * @brief calculate discrete kinematics equation matrix of x_k+1 = Ad * x_k + Bd * uk + Wd * @param [out] Ad coefficient matrix * @param [out] Bd coefficient matrix - * @param [out] Cd coefficient matrix * @param [out] Wd coefficient matrix - * @param [in] ds Discretization arc length + * @param [in] ds discretization arc length + */ + virtual void calculateStateEquationMatrix( + Eigen::MatrixXd & Ad, Eigen::MatrixXd & Bd, Eigen::MatrixXd & Wd, const double ds) = 0; + + /** + * @brief calculate discrete observation matrix of y_k = Cd * x_k + * @param [out] Cd coefficient matrix + */ + virtual void calculateObservationMatrix(Eigen::MatrixXd & Cd) = 0; + + /** + * @brief calculate discrete observation matrix of y_k = Cd * x_k + * @param [out] Cd_vec sparse matrix information of coefficient matrix */ - virtual void calculateDiscreteMatrix( - Eigen::MatrixXd * Ad, Eigen::MatrixXd * Bd, Eigen::MatrixXd * Cd, Eigen::MatrixXd * Wd, - const double ds) = 0; + virtual void calculateObservationSparseMatrix(std::vector> & Cd_vec) = 0; /** * @brief calculate reference input diff --git a/planning/obstacle_avoidance_planner/launch/obstacle_avoidance_planner.launch.xml b/planning/obstacle_avoidance_planner/launch/obstacle_avoidance_planner.launch.xml index e0ddc12072bd3..478c692989aea 100644 --- a/planning/obstacle_avoidance_planner/launch/obstacle_avoidance_planner.launch.xml +++ b/planning/obstacle_avoidance_planner/launch/obstacle_avoidance_planner.launch.xml @@ -3,7 +3,6 @@ - @@ -13,6 +12,5 @@ - diff --git a/planning/obstacle_avoidance_planner/media/behavior_path.png b/planning/obstacle_avoidance_planner/media/behavior_path.png new file mode 100644 index 0000000000000000000000000000000000000000..cc99ffe44e1ec2e5d525061760792df54b4bed6c GIT binary patch literal 14745 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 mfuCVyfOBc8korzS;6TUl8-)UF*$!upIk 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 Y4Hrst`;Yb1@c;k-07*qoM6N<$f}Pq9i2wiq literal 0 HcmV?d00001 diff --git a/planning/obstacle_avoidance_planner/media/drivable_area.png b/planning/obstacle_avoidance_planner/media/drivable_area.png index a1c855323e5791c92787f63e641a11811a876910..660629970514360d2018667928fac6c4f341a36b 100644 GIT binary patch literal 14299 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 E0HO(D8~^|S literal 62331 zcmY&=2Rzm7`~Qb*k;q79NVZ6bBFc!6dC1||*_#lKEqjC(B`c1-9a~m}?3I=6*faZo zAJ6mqe!qV&ucsb9=t&?5B2!d&tPVj0MG%DZi--XHgh2LD z7x)9uSw>Nl2ze|8THkIYWL^rm z^6(eu=hw#QYY;>^zqzS)e0+SgTGF#SYVr9!KOrQ@+2(?iS$n)*ePnoka4rOwGfsG# zQ8sB<1dBL*$Y8PUd+b)7r_)woT<;6@R-(~icbmh;yf$jHKA+5SW_Ci*+)s&(nlxGg zgLnLwAmn4#23+K>5RGx3FvEN5h__EUY;9~FJ4y97#fn`c2`Zb=F|e_*p)ke?Z4My3 z;^O9Jj=l^@ldr9I!7I^YWX8wyeg83P`%GFoDR2ZLf9Uc7ig zHMKFvS4m=hm;S!^JUbf*0pza9TNVgP^EsJi#(*o2MtgkBft9#~`P6h17CmB_of$PY zhHBkz^BzbPAq%@!ZY9GY?fprwjpeEH)2U}kI8gg_IbvMMVHTKGUS1v#)}DJeh*$kJ zYv>OLd`v8c0%d^094_`eOY*}rDrHB0lk_ji;`ctQdUxurYdzh;KEgv zl=&qn245jlKLqx1@LBEa!#404zq3{O%sTBiua-g$HDhc01D#Gcj3?@S4HqpIGdrbmU8~{!9FRiM!~{I~ zQ=$D;sL&vS5)WyU_R*})PT1y zmuc)DQa$Am9$576U>F27ry3Mse#HM1H|_Gw-p#ikvEV)r5qyp>;23xlkFWh}{rPeI zaDSHSp1}FGK&Db7!`I77U+-%(JR)T!EkuK-?CHUKq_qSi~fxrD->crGDi>|}GhRx%Ch zUR#1ET?3opoox&tye}ak3@3nCEUe1(*5!yPe?rnVaSwBN8GbS_FtqPTp3ZSf=^nib z6oz8-t1cI{%b zVB(lc1B`HK%xr0EC48sHai-}8j4rbym_6CgRBFqU6_OpOj~ zx&Gl7S3JMuenV+Gf$%C4r+8!Ldx+=$>UCBc$drMVMi3vn^&Gvtm95z}5GTPc)Wr(R zBzi=;$U5fJCHbS19T2~(R-_-!l$biQo?W&h{YV(V5JP5;(PWR7+Y-L z7zg&Bw2sgW7y9z%X3UR{Z+D>4vfA3bw6rvsTv&#s^83dummm+PSFgUMrLp1iLhWzB zCLrjst-HGpCuiC*O+1=V?&cSq!iSE5td2|)&yEqEwJB#a%xA3Iv$#-YCH70Q7f5){)%xZF=J z`yG94gU33)fq!J%t8Q7yA>=L||0PE}0hh+d770D`wO$9ib3fvVP&jhEU48QHxVyIY z_LxDG4mMp8d`u5te>*+(_TC@0z&btmeZN)N0@_&HKlZ?Pv##!J#oA})yM9>9SH0T9 zI(eF_@=I}v4IJIGxVLO=ZI^d?q(tFzR!WF13fbq(C)>KB5vUq3{OWjZI)~_&h zC$#=#(#DQU4+;7`_lJ;@;s8}jEP8fJKA-+{;ewz`LgFe8+Dq!(Nds#xwB+Vr$@SA; zHC@G}W%>Ar=wYAGLmNGXVt`X1R(Xn#Ei1;4{)7u8qSr#0eZFB4Mek@If_VM0&#n=Z zlzhvMb^4tw15}S|ORh_mW=+_f;=#(nkY~O0Wb>dc<(}j4bu@sN$NwycZng2~?`-t5 zBwR?aJzJAI(;9}-%gbEs`3y=Sv54)#3kp0XW?{0_@W-3DxnKVWV<0R@dfXWaz=zr^ zl%uFVW$5LZTm9}Y{l#I~W?S|mmPs|tx2F@iacm`$R=_M>{Pt-^hW(3w&;)oAtO0pq ze1!%=G$@2p%Wn#8-1*p*$uA(VQDeZ68eUpAjo({jI}Sh{T*z)|Z?F3K_4XUALqf5U zc+$m3m3#>{F*;yt|>sFc#=P*095BPp|v@DFa@HIa_RdOse#ea z_t^Wz8*V>>q=(4%5&su4?1AwaF;+~Ai;I~`(j?U3Ps%(auHgxe{vZ~J058Vc^w`0U=b98j2-_;!hHk|Bsa(1 zjGNeEzzg-)987v0wFvkaVT*I?ou?E~j2SlPrlvR>aG~E#?>!JIZhrQ(*w;eHZ$*Tp zkl?fY?v7zA5k^MF_xzW*w0LQrB|$7kOp>p@l#~c#m*9lN`ak!+q@*dBmiD6UidiR0SrQV%q`@e z980}2#2Tbix}%7eR2o)mFg$GNYy012KLLCPz}09Z?B0LpFTrf zky{b$IaBFT*n1LD9QL07*>h!W?E~-a9~Z`8p}fNU@ZY*_(w_lA>P`IDNIvXyDgm^O ziUyhBcljI0110c2X>viVe?+8CX0D8aIERJWq9GPr5|wgX1E@257;|)lDZsK7@RjZG zUw6~QeWb81L1!lk7=GZtbbZ<*WtkFUmoC#AoNWUy0A45rD>^+{1i%AmIL|*!;~@@k zolzBW5%OChEosC5aJXRa%|^oe4<3kMuZ|s=nzD#sfe>6(ME_GIWkH>q8~>k87rIY+ z$QKyY3jOC5Abhc==jEWq)60LHqoSg)%gAM?wfKYikIlKRk*v1hI;{YvfVKS`bX*8Q zmThz(Q?EH)HO6s706wA$SO6CPE+ScQ-v|flTi@J_xdVbksIsIlnm6W`JdPZZ;BBMxa0rXd;k5i`o%rMn~`#|oLL~` zq~!ojf5(pvlIu+mc_C>%MkXdJU*FFct2TSsjQdCPBBK$J4^K8_y|-Fuu~p5K?!o|N z_4Nf7Jt-jXp_n^S%ecsN%$}!)fQbMNc;454o=4V7)E9&gh3YNH=6dGkFMgJa-hEdh z6Ils}smZ~_vP;B71$)`JZQ6lUB)o9`;5FDKEC6gjo=R>27d)q-0TD6X#_VeYD!w#< zc%HU(_Ki%AUxmC%=UdF@SDi)LOZ?$1&A3iq0hwV2Kf8$GZy<)TV4@Caa|m`bBVx3) zw3*hBdKdpl+^90G^yBjK@?9)AVj(I5G!HSi0My;Tf8Q9yFUvjNT^#LZ16~&9|3NaL zFmC`rcmIXMX6}BAsj2hEwHH$8emj7$ei8|!?q@-l!3twgwxaniEzhJ=K~ zKUw1R1wpHv1b&aaHa1K@uZO+Z{L*gAd~L=gW$5?u4fo^n+ugW``W> ztn52bXA7^LX5_PRw55Bh+I2yHKKFl5Zp%r@*5!A>Q5{;0wiYO z|6@lDc(ebR({xj2Cbm@^FmUPXLA43pGAy;ZC*5^pzU1h?nD2M=-1z|1=&nlUPSV$KcB0z4zL~aF1Dw0@$|I9R;%^5!~D6G zsz046ANoZ2bn@?O%*&d~=3@C%cTW!hMFt~~fA9+){A-jH3Z2BZraA=%7m;b~nB2@L zg;QT&uS%Hd@GneIUzyruJs(DfLO|UKydgajWP*WKNHk^A2ONBMlV zn}szbWy>45%0O%Mdcxk=x!z%ktYdF68RhYOzNa%(!&Mj=dr(CpWpRlcT%%j!s zO~u;c#O;b>dtbh@R&W0;v%O32xwn!4_($Z>RZ(0TNFnoQFsw{3(vYag4R{>HTFrLp z73)c=fK1ha1sq25(U#$RAdGlGgn1tAX7|!V&|$(Tg;E>yk#V6O>FDTUEmr?9DDsDCffJJB=-Ox;r(BjediG;$W#A`9X`Nmb+Eq=;4J+8 z@QigelLy0Dj%4Jj^qFkL(a)Lt44<2xcvY^!Ehf$Hk&Dw`6eun@hD z+#8#i82s@Aur-Q!`DiA`IJakF#Gm1>ZFPiIL(NEVNTInGX}w=SBE}Gr!zHA5xf8;G z<*~3*d5LqngimRf8_v8hl+;zlyu`WVN1qPutoqnnIS3tTYIE5K@P{ zp2hAHoYSa_QdKi_tnAP0T7bBQLmG4WV0|JDyFQo;0Gc@VcvkIN?ctpZS}fiQK#OUX zb9&1A0A7`UV|42ru)~NN?laZ1In~_?-{T5AUTf(}EVRCp$5tInq3gytiVfhliKZDk zuzi-N*mB4|2x1}Sq4z}w27_yCwMF-~=aoRyyEqaabQgB-(+?#Jb=05`92a5<7zx3L z6uv$J4}=useo&Aq__MFK02H#S7zbsBU@leohbtVACh570E4k4ClN8@pQDDwa0f0&X z0F8N93j<6kWl=GXBnY=4N9NY$x_Smq=PM>y3@UWH*ekcQ#rcJSJhy|Va)!HZCEBT?QqhwY<*uVF2J1Y_Xt4qE(4uAZLgp<_H~?h+P46)%c(wh*&% zPyi2N;tvBCj5~q-%{0k-6QN2Ho}^BzdmmfZnW2~m7xp8=zB0X2KU52FlaoVd>Q&rE z>WJ|B&Bn?s#oSzt)Yxs!U0lW9+5Eog5zOpGxz!61TweB$9Iqmx{602J+UeKCx#S9Zi#vwh>DDH2_uug zqWlHoyt7@^b4^f2otbqSa|T(hY7Ojd4+huVhMlC6RV@A-U#F_{af~{a5EBbs8?k)b zI*pZcq3y4|FMol;uMqS&1+LHjj|TVPLG7N;)Pa4i0Fgfom#V)}fdNQXW9vL9fc*7~ zRQhIsSQKB?$Lf4=c(y*j>T5|H2%&pnPh#wLOC6eKjV}85+n1(U(&HSAqf$R5^qd>bYI6@!M z%Wy&lNvBliD}@5X;9W#BLa2x6w}W&z&G@#j@K`>#^Z^ig)Rc6k6&C1BGdFvku8#Oj@qjl?WWN>3!qI zjW`$U6NL7il`HXq)urjCfqE{W4f{HWUf*;ihaioMg3vqWW=y6+RIZn2MB6caC5Euv zcFJ?ArKM$aLp{4Rkv~s8`z~cVjw03Y(jy2`2DSqM2F+gFn-DAPKVj!SlFjj?KLe|W z_V@QESYP^MifxO1|Cp`0NesjF8rW~iT}kXTV^Rgpg(Ex(@HE740JlKKVo9LfDt1Gc z#?azd3yL(;TYOc*@P|iSk_3TRL^(J3DoMKtc31x@)@1DCUq~^3|Be7r11a*OuWIyh zAk4_o#S%x+h}I^%Q@n+HmI6aUw_Ghb#D4)yZRZ(w4M}bWiqegXa8-I8N_`Kd1Z0a9 zY}!7(v~bTFTu%Vk3qbIY!S$ro6FTQ-!a0GG1H@;S11u~zkf>7|N0Pc*iWGeK?r6Oe z!DKo`Q_TH4Lxkz&1J7Lrkc%rWiSI0l=A^6ihcLqh7mSx^*p(P$aVsb%Sp8|aKMN~2 zAuO}WiwGwFVX4f-WdxjSoRLg;%G@;JR(nJJ%U8)unI3pH-@bvh666$kvFOgnp&~#2 zihbK$deo5qZQqzL0GJ&@c>j+z9%HS+BXrdm+wt!1x-7Uh!S$kMe^(Et!;BvJrK)4 zw6)GQ8{H&rXTuf^P01c%Y?*1phK^~=m~5_?OypW?dCCKCx5c7o`1E}fsdAr|?kh1( z8Ri1`f}rIlpratUBIN@3!Tsi9fCvHk==}?~c`Zr6R)IA0PN@hsFf^1nS>s`X9g~2s z7*=o@P|}b;+ndE2|DzKlGPjC+GGmK+7BqbQ{5F6|8UdhdG)- z%T2uqDxz-U5q5l1QcCIU0ot0_SU-2b?XkXrU~(DivFzQEV7KAPu=eD1eBB5tyq{^n z4-^U5e!1?b0}Iig0L);Qs;uPANfU#Qr;GHMu+z!MTr2?x0Q<$ZoB!o(;HpqCa~tSP z!ZN(vX4EV2Yo|!;!mdg7n1B`?Vf!zA_-XQqMTpJP1ebP3>@^R_)bwNw8XGNTTawZ1 zhLH4v;{rqLo@cE>l&~u8ldS$Xb9L)7@`N!=z(q)p{qe@G@ko)0#oFn0o{2_Z+sKn@ zHS%lvpdmRmIi7;W+Yvww9e`{~_Vgzi`dDIP(im`do*aZJ6XyP5RX!va_fNUx2CCE@ zc+*px@x+Z|tT!RpXsH@E1H`GNWn~$D08SW0hO4qoOEY!6T@_Lq)JkI zD+o^MM+>pPS@b}opRSvbLoB-F3uk#gtaAt540fvz}f&dp01g2(jZIe0=2py!nG7N6SFjG@4;L3^7PSf-Y8o)#4Ai?~{%BOdGBU5z8Efe`fAEZYtS+?WD1o|q>9LN$m` z#VuM-_=*f2wb-UEx#Jo1t&$78`9&&y??QbQ1+8=wXb@q{00kY{aytlh^7Z~`ZDH=& zha}w<-&_Gi8QDN8O>&_H$^)#oD*KRfF(y-PsFVij$GM8ajk8ssHWQ$Dr6>wFz%zi9 zFP0n)mW-ndBn}9Qv#u!#`$7`;@7@S*3UO`5$yL`7B;ir*zrv5hMb|*h7b>pg&1PIm zAj~x!iAANE$1~o7Zr#|(zggIH*R0BF1xzks@tdl+JiE?~IfNwax^bnVhxY=0@x-J% zWYMP5A|)=CsRK5J3(_BUrESKDLi+QmTf-zbZ;Muo3$a4%qa`y%k|rq&nbf2VHMM)G z{YrfbOB^x+EVOhSL{K|Q^Ke*ePAf;2mj#85g@zS6!cbR-`5X>|>(O}E zk9BF4k48n!e$OC9aAe~^*Zj{}0GdEc847d8=*gj*M3;=eVgg~$h#y&!zk z=2z|sj}iDRqvof9Pn%Q=UNjB|e$H;pwG*5mZ(3L;Vk5c)O1l^c!c_3$=5P=OoM6S4 z9osHB!(e>n|M$n$`Yjfll&C^SJR|9;|H-Nd1>4ZQvn!LshlfDs6JisP$Opx;_t^c@ zVg39`N{<3qPW~No3TV6QF@+paaQcOVH`H$!KgrPKzFut~@7M9_9T?P@t=8RnZ#!<~ zjU}M%gt$EETDtik(0AbkLQ$VXJ6~`Rfuycz67UxQrQ@oj+2!um*5EC*RJGf@uRlh1 zkpZy~NF)-$VcTuH=QzL1e;(i?55++wlz?d}OA=8o#B!tftY3oR)dWesxeJmE`>2ay z4h3J&jvC!S#+?5`{1>#LsgRB3pACt*A<6|LHh z|BQIVWj0t*j@di$3V0Rv<-&N2f#e#)y+4lQNA2{Tx?XcuPS5vGM%jAq`>{O{n%B=( zw{I@tBBYtBb%#9kAyJ^i;-T!cMuDS{{Jbpm5%_rNSe!wM{K;Za{x~xK%dm5Ky%QNPdn)P1 zp9RM8v?MAgzAW`@2Pa8fmy+ErUt;MokSc(25E+32uMnooI@~%K%zo8rf_0^VJ!kgG zbu>jW1eq(^{wgLC&9TWv_Y`#oi+j&>+>E}V# zl;3d*?+NMwMpXCV6?SVw&94*nwHhZ2(gwSnRzrZ017z^JXqk74W0g z1o^%Zk~H-s?8^x-6IZb>i!tIPEqY+(IjkbDa2WtKguIioIN_baaoIq3{P$9Hj-{=B zM;$GNHjZ>h-f@;#G=0N_d5}IEj?U92uu33({g($uj1*oXo$Z(}?wA*1!}kjXIBQp7 zjgBY_y&3VwDqT@O*-ml&i?#jkfp95zl9h+f%8E?hugMV2hgUDLC~|l9XB&Vai1pi+ z>D%GVRD#j5NNc@Hj_bsgfS-*5jYgViQ3ZmCh(jKtf<>PP@+gwO{3XhysYp+p1DR_4 ziv|#jOt_1^VlSb7!qX=@I;$`6pvQ~T(+w`fJZ#Ol>L40MueG%oi_zdLW(O=(Po3s3 zQ^u$TYC*^+ykql^b*{zOl%Olvp9rP=+L6Q>q?N+>y5K{8K~l@5n?vGFsQ%^Ld7q)J zJBJUX_K)9jte*-#|DiF0%i@Wr1$s-~tEM=XcZtfs(y1x{plYzqzFc%6`l!lH4rBI) z^uBYaw)~}v^F7M){qX7#eNdLmv-##lm44J>WyW$F-~E=1C|&LZ8njQI_D75-9)E+2 z?r6xM%{>j*)y~dWa&~;jN_D4h=j;I!F{@s~_QT??M2Ls^+*Vw5pTD=~>qg&|B*7Ui zX@4byxuKM@xZEjFuWug?p4@tY!Yb@jB(a zWBoDXejcQFO~*n>53RRA{Gv&!pC_S1VR-7k1oZuL|F?~#UtHcxIe@52k9lmnWOu%) zu2`iV7H~2!ngT#&IiG$<&9U>8D)IS7-sW=)nva=LwHCA7u94P&XZi>p@#mMWj zVXWTZtVA8=e% zGb&bml<*ja&jRmo8q}0bUg+P{poEI8r6xAtmcjOd?)V~|1mgXVuU6{o z>w__ea)GLy!Rfnns&x4_c`#%T219n4Rt7pejfAIWbOao!0aly$aH!2$zu_X6bZ@o` zSq?4wOc>IhF+-y+*ZafS*6E)pR0473{^EF={j=f1LZ_-+!}A4D*6EB1X^>ihmw+r_ zAEXgKNn2)Vaz^w`u#4qe9Njq~i=n>OfZo>Nwlw8tC>eWOt~bn)j`@k0H1=sXLF4kI}&t3%DVI73>JSr<1i% zM*Z3bejJDR!Ori)hl!~gV zN$nLk1kNahJfcOc!R5ulL*9z9D>=A|gYL&T2{u2n_owy?zUMr0m=C8igJMhpgaF!Z z-L!+la$H`PH?>j$&fw%Knh-6I;37{H;i33nqHAGke=g@P+ zUHYE4zAj?CTyM%;qB)GG(3k=7uuE9QVVqpkIo;|G9f8J;jI(a;dI#Y%d&zGfZZRNq zht@Dxc5)sSK6wR-PEN~pp{u}__628z9Xz(Rp)W!TOOM5H&fwH5J8$%)|^_eJ`bj2lwVB=QuM!E5^?hQJAwxeWhsEp zNa%=Isa<_hBk>_kLn$d74BSm_t&PMTu@UD@Z7R`2?_#Pbtj8*)Sx`AI2AkJfwk4w* zNV4w&cEPISsa z=mNPWPD|s1x6~1?+I+q5T`@ioBv+O5t%X>=8QJE=;w$NaPA?HnFn6^gyK09covu!6 zj6-K;ETAn`N`D1KLtFrS^Ky9NxrRi+t%%bb!5YIlpDL+%Tz+RK>-Oax6F~3gJG7J} zbrgC0Z$i?aPHKONzsER)=kr0K1gtbMIF1`&)2aY&*U3?jM2G$!vxDZOA-C6u3Z1pE z{EmLqL&Vy2`jrkX2w}j@#HqDha>2}&#oXQ1w#bAOT8KZQ2xo*z?|K!{kmcR7y!R1L zTY;g?abV4Dqh_)>8Aj!wp_{B*alatb)fK_oDD&(Fgk z!qrG2b0w`%*Yh&~RLKCS+8KIwN^5KREQ=hGm%7X~z;{wo5}tN@lv?Gr^)K|qN#?HR z&z6?ZuH%jKF8Vmqe-h@GHGwMYC-RZ{*44W>uvU;{rfoN6T4nh#a|=nbn6eg7iIl*| zL=Y~+4%A+pcQ&!Luo-V{%2aB>?zmfS@JJ$=#{Vyzs$`M`PA|l41#Z<8exNK|W*>3D zo0(oKisF;ugFIwIK@(b93ws`<9YOGT@;5yf-TKcDcWKlr+HfaVYKqNQ`{}P#E_DNH z;ri$GRT2RQ9OUa88z2nfFNQD}Lk8js$W-%>Z0=?^PtV%ti1W|q!{>Lk$YWpuwJ=n$ zc9*$tE>0?SvH7GoY*x-c`w>M(5tU*omUY&Q-fH1l*Q7tFrbpZmAWJm{_)J_&=Tg@s)Hd+4sDy=Yf>qQ3F_vZ!KRtWA0=k zfQ1u6C`z!e9mNWA9*h_Q#pgQmLy_p2L`Amii9rPyp(+=f;~Mz zA9eF>aO2S5+cKa`Nef-SQq**Tf0wm!+2d`;ok+Sj*Pe)cdw!9#Vm)NT~9-$48Pux}5;3nCDoEM*{H?%w@CLnmr`ZKmOi*FW{RBq9W6G$T#g zUh>GH%;sgwOX=zI3^(JTri?9bt;P}8TWaBTjxXRK$)hhjJCa%H&pqCINMebG`^2q-t95rGNRSYpqi@?|8WucftaB z0r;c<`*My)1{j0eWubO}7g+FzZWBT-O9A=()t|3RcGS+3+)BXM>}?t)+(G6T0B>ka}AJ3JZdlLY397q5eR&>*=!f$3A(`hK5wC2 z{e%dgj%LzTgy@T1Bz_Is>kpp#%i4t^HLp+TG~6se7Y)GgiS~mS>aki^%?b}a2lvY@+7a?b7wEs$LpwR>e_L0%d?AQz# zUOK_;EBG1~^}VDYoVsIh8Z}`};`s5I!s#$jzdaiqjEFor1}&c&2kMPvGyQop?|5`*je8^mkk<5UaXQD=ENVR<+!>&s`%Pf0h zHQKiPMn!?iRP`Z8Uw(glz{4{z#1kBqtY64K)|rucpq9$CyW!sSt!b;GnsaD#ZRR-t zbDLXq2D#txSncW1UP-}beBlm=%D1K&O?tO$e}m52rVZgC)>F#p zq21{3J`&d)K&}KD!Z^=DaLoUOM;y9@6N}g3&IEo*b`R_ROdtN9d^tj07sZ~CUm)sp z+#QHXlpVHxM%4ZL*L)YIvrV?b(u9TcbPevSpO}s+^1Q_;adH;<%eF_hjJ#XZPNnMg zraRv*5OJ*g>U(7BO<-|WFNP}^4N0$(f4Oy%ZBM{*&(G_uqiQ-aHFhKg=nX6!#aY6B|Jh%>Y z^RCJ2l)~a5JSp1MDAL8H>(xC`N+&KCh7=s!&=HpOn_oIo8iyV3{zad zQ?#Cg;ZkX=i{jh4F^Kn;2$&Y?(~#wZ(xA7d{pULmklH#wju4BmBGH5IGq{p<1M>N- zw~4>RVz^u;EUPOZ1WtStw?DT+$^F_)LWBNRdUL)_p}k&HObHR<3xE;V*9X)%yr!cT zrIJ+LGzL2#y@h3Y@8`v61x!Cx&#cY|S-s0AtfGcq4kQUt?cdkM*Fa~i|8}M(X=?&U zVR*v|xtUTH-}AFxz2)w%xIg4mgfCK3WTV=^h@VSSFWfHSPr%w?E z9t_o*uEu5KtIbVT#bd4_WZ=+Li*DaQk*|jQX{2HPbvArLa~F$=``uQ|l*r4h--Obz zb4XS#8b*tEgN1_e zxN3pbzdFu)a*GILN`0=h9x$vF#*7Ju(v!ByHRWtNw#rj&3*BLHCqlEuoKoXLYp z#Yc~n!OWTW8)zBPbi77Q8#$R=0IJIXQZ@Oc(`nYw9mWpe&^bTer-h-@NPL~0x)?&~BcK_&zu;Ky`_RPCl!l{Rl1e7>L|$N`6!-$07y?i!x5 zKBu|Zj5BB3f%oWs*BiTi1Zx*~`nI>B#iGS`{(9pgI$ge~k`y^`MrPm3XQ4?P0VnW_ zqozN0N13eTC+o&Xp?V>kSD%QFOPX|WVhjQbbB=8sgi;KH`oYAyl{mAx#wbZ7twgCq z-klb?w>7t{o&@G(g1;w-v{? z$UpC_+dz$DIu5)QPp47(`~~{5>RJ^!*davFYi9X5FUt&j*hg2lQnG%oYIqL&Wy zgg)mRm`xJM{Cz3LO8xKMB4M6u4nY$6!NVnXQ@q8*Pj!y=mODum9(|4Ai%!0 z;EpepSp+Gkk_%o%g40#u9@RO2UfufKHbpKLS;QifFCl(@vg12cJ>@m*!syO@jU-S# zrc`fOi^;62^D->N=*wMeQ$(eP)k$%qM1C73_dqK3!vzKCULI+Lg&*=c5b9|}n2?r! zkaFCvn}c&C_@ovKbV)rGn`Yq5I*YUU1Al(6QS`6DMB{Kgl>l17#e-58QwBxw>9tQw za$m3N=Vj{im(Y?{Er@l?B-aE?TlK*n@l^VpT@B2)nD{MNvtdP{5E-a7dvK?6GNLz7 zw&oKt>;^8+<;r|id^{ECs|uQ=yjpPut;gd19S(*gx(krgq%hL|q~bcs%ReI`32!Y{ z6tmgvK7D}2%&4F|Q+*c+2?-a%V6kk9;IwrhOo3s%{MeWGxO>xz0(RBl891AW@Ci88 zK%W&+k!KJ%pXXvMYyu$bKTC~@Z9~r-sMovEBAyPbun20rKTi<&EE)@!>+DX`NrMb= zW@w>?FT8HtK~lkzr=C+aD>=@(&x?LPHxs_p!+MKP*bqMrZ}E>p+|+EkR9rwl52T2I zyQ`6*^V5lwb?C_|n~eHdK7A;iTFLK5IdG1o^YcTUV7h|jl99TSM{IH>>$LQ;xvP(* z&1x!9V~oI!F*S{6pe!^T9Y*<_Z_CsgXq3OUwLQ=r-Bs2p6O+k&OczCkr>s|s37fXM zaz0n-6;oi`p>iy6+Nh##_IMVVC`=!HoGs?;%H57~m-~f^S$Z zLzK6}lb?5nuFRIVo^^ld+)2z;QQSMq%*E`lcePf{+nG>;KMmB5qWqefr@@6965 zYYc7@#nWJj*zmri|F7ODAaDC7s~%4zF7#e1Jc$UC|%UI8vNG8#YwfLV^>hhHA{>x)}$Gb%90GLGOJ=5)89AYI(RxzoN48-)lho_ z%QgLHo-{NhshLciceNdQ?*vKHkfBjvbO||rphi9;ttYAzj0O$RP!X8t@ZOG^61&B? z6WtTNw=no#rLIP^0E2?v=32tHIv6YVO3d|l@o>zrKi_=%BL%Lk_0;bN@GCv2&x%{TUCG4Pzw9!Awpv+Tm3flIf};D@ zZ=2n3iyB`3F>(9lVsg&YdH?%sQDbXSD+f8-UJnM>wKRF}vzc5CU{R~kRqu5xo1u1f zOt*-QLXH`Z__ZeU3r|X zQfVE-hgEpwf<+@S9?cg=!ujMU8J@fHe5vMD3p@Njr_WuT1L=cYw?V) zxL%@GQcqi7IrlnCC@;U~>O!+LxD`t>yy!LFmEt*(a-Z*ox~fl)cF8Ds0V66~J1fmD zE3bPmCq$+((Lbd;$BZk61mF6vxHl#*j9saG@LZ$NmfA*1rRa;Et41-e778>M-G!ui zh=TyM&*Fyy_xciVXMTBlk5q?gLQk)(@@AbYx5Q_B!SL@nH?@)roTEEJ7qI((&H`Kp zJVQJJUqi!?MOf7$(VS5KyB$hgCV%F(49g{OnxJ%35}ZlLo)#4{?wz)#^9;fjXv%lF z7-@jINon6ZMPLs19qptkQ;=6Hn8$SW=abMG#_y-Dw~dJ@$iO*FRVAExpgoUpFju(qC^bl`k7>RfuhIJvn~S$(X1 zj$b{@JUi_BD$ObT8^9ucd)YY6+jwIm#ngDGOT3$>Gd%uYwbJEUY6Xj^Va$Fs72e8W z-D|_*HR(qMoSl6RxqmRTDPb*fCn`@2eGUn0_lAPe6Vl)lNy+5}{r$rDk5ucA26agM zQ(o8ejTaAExeYA4Y2AjQ(Uos)_l|;&PkS`7VQMWrPyKeg$6d?D;Z4 zZmjx|Xl@WP-N69Jt$Ru9B$MPF#y|yOiGs}40{YQ6)Fk=-#67Pp)`vSDPf2bEIwAoh zw8y%uAlryG8IE<|dY+kYHLOg@W)ee~^`y*DjQg-}tL07DiyJHL8mfL{6Vmg^WIwpt z>wGwDvSFGgC8>HX@l)0vMQD<(HaU3+=nuW*mNvlM~7 zo=Dhs+4r8d<(d-UZWR2wy zYw{+}qf__`^tkVH*lLNI(K3j9v`qm26ojp|R$;nT_U+E%d1^-8;z2ui7yFX0U&ggs zxNgS`*py$o)PIL9>`LV3q~LOJayVqU4C#N ziYL;*$ND$o@7ndogbd;~d`f?;@7@`Cn@o(3KGC5?nsFGhBjUPNPww-e4r|iwdU|yi zBg^USe^lwHslrFg^NKM~N!aVN{LN75Ung@!(!YRzUL&Lx*WkQ{95-PiZqlcMHA{`o z?e>r2{7WCC%gx~R?f2*=>tHb_Dc4}9`F!|&3q!&8*UCM8bW~EmNSAkH#68XU%6)^n zYCdj#a0AXf8K+U2-oWJZGhkXVu3M!byD6b-K7~;(K*}#1+s9AK@NyfyP zAlagEPo+p(e#+OG|HshzG3q?gR+n3QrI>mNWpCI~obTe}$TeQR*JdA8_B^|tFG-8P z`m`t2w$piH#$+Gi`<{KYJfop_z^eSn&~RKE1aI|(_VFK?8+N(?WXLMgwX4&lj^htB zd0rX6&q18=1{wUMbe#**mYh83!Q8LV==P76KGc@tL1;$Hs>(eBJMjGFR zER7#D-0y`4NrDsGj{cC4QEEQ_vi8W6UuBhKwqW{!e8gE_-8Vh~-|O(}Fw^nrk6RmC z(F5Ire2cm+$!o5q?DV*&PMW+f1C}WrT(sBly7Nr-+o1iPMjDc!*qtKnbCT1FkXfT# zpI_G$68Xe4zVddeSBz`rX=RZa2E#*-pW#zc}+@QuItrL8Vyw)R4%ELkj zxU0wD^S42(A6(R~F57bFba|U)WoDx_YE0a&lG1{XS#LQP2Kn z+!*Vha^j|XBP#2wZT9qCGsPSmPb>E^Ve*H6y?%ucY`AR)e32sQIT*z3%^laB7^ni~ z7dNo(!j4aJA`L#rQO=iK^H8I)=-S`Y&$C_HN)vXf`8t{{I|J+bD=XY;s8KCfRIXe9 z@SvG$3T@6t*V6E0oXhT7QMW+%HM}!dy1PqvR}8-wA4T&GpEKy^Bqx7J_laTKlhQd| zXXo0qb*(;i^exlVN}@fbKQh6o&au^UDKO|?w$y0(nqkMO*2w>DNuf(ymAZmmH*qX4 zpI4WgCVA$RpwU%qfiSHRCitzt?yw?nriwu+F3$&|{#)ni^Tqh!++w54lS6!Gw{GgV zHSTIva+wNW?w@S#xUThEqn}c)EB4z6JfPR5V1KzP(OJmbP{{X}bfj=bNtU_6{a!BV zcU(Nud+en2e2F4`3Us?KCHp6A?tL8m9d4Ru(IX;c;$djm92Yx2y0W@=h7Ny!-Rtqf zkBGlgTaxF{l-svZA`ZN$Yn6$Sn2}de&(an&_PkBq7$kc*No1P@9}~TyppbvA_y2f$ z%cv^5?`zngL8L*tyCn`GNT+mnNOyNgcX!u8kUSi^8v$uJG)R|#wDfy%|NhVWNrw!F z!@aM)Vy?O7Y{MouSJCPJ(L-T_gk3uzLm{X8TFwM!Cc8 zCv|U=R^-9{x^w;;%r>+z{DI~QG=$(DBX;62nZzUVZ>aKmg&R9Ysp=;#WP$f9d#IhS z3=Bk$%&4DvYy&FnGd;sk8DdQ9pMN!8gdt*9bZSu4BZD6c>NM*lk z#zpjAe3k1{O#cp}yo<5xfR_l`0@3|*_lIEAm5(TUdAlb}rR3wE6Y0*xiDtihTA3sT zLaK!&YiLtbw5;Tp1`W+DI1#-FwXsly)p3jomjt9FIO_&kS_3d;zFAutsON@>o(3sg zjB&hdeclK}kHX^<%)=qj==;1TF&G%})RzzrFZx<&XxptGL z!3VTMd&H8>Bc<6m)#l^CO}?el`0(GQf>^Gxo5`_TV!F+mdwqNGnW4WZqfLV}gwPFG zm2Dc4oZOIG#g_8lN9JiUS2HUGn8Aa^il{%A6Id;R4B(X(MtYNRi3KD_2-@a)9{&(+ z63y?s`KbLU@1c$}C5+}y=^SJyHqpgngmK}g|E&<3v&J1#IBJNqqY5o(nD4o0cP)0+ z2~jfkz6^dMkH()BclzZ>Pz#-tEDz~nH^M-e#TRVtKM)O!T02~G+~NVf}#tyN)z=sQHvo1y}#k{*Z@R;-!!Hb;Vr?2pFKw4kh` zf|McW;=$tv8_#Mvw*49VCPV2x-h^Shx`HL`U*W=X}iPyb7LE663`W%!|Pu-4bCDq(JXlv54OCs zj<&n{U*7ppv?(s3<-d75JaUXMZ>Vij%ogDqI3i9paf2xnQ*0-AU^0q9fmNx2|Ge&> zP;J^aV6YnG&S+F74ry=i?^OwwHA^)w{&rMaO-G^XRI<>?>|WETM6zsNXctoV{hDHm zBPya}<72Oq>I}F0jfqZ`J{HN0{V7(M*x?obob|g9a?(L&R_>WcQnYf7ic4=ljhZU* z5F-%5=6dz?5jxTx#g$}e)m|G~@vHfUArlL@nJ)NZ%btUX-l+>pILAu=qK49jWpg(( z_|dw*?Ux|0RFv(G7Fk@Ps!=(nLYf?X^ zG%Bu?rPT%>YcTzwhkkpM6tP5EWjf%gWGG-pOkks^a_cc6HYkOiddWU%8R%9F;PTP6=fX|^?(Cizkc|VQ;$G1_(t3}1xELW>z$Pl`~$2|du@!l_Pvt}o} zk21EWf@fdXz@83tv%|GhwSo$x$CGS(f$W?<4Iox#8?2$TL5c&Sr{cR)Tnt@L;wM?_y z1?{W&PXDlJWX4FZ>r1|?(tN&w()cE$R#xYXo4QRtxSj;b4#^rarHb3~=JZD*D#IR# zzru8{g1M{M+sIcmG_s%D($~7lLqE$#BDD5lgisTpeLmm{o4S|^+~P-Ka3XGVbf=NU zy^DdhE<91o1lF_GG|&>4KGyT_@FE2Tay#XQ?!GC6Ep!f5N#?p&d?Dy_Pq@~PMijF* z)HN+Ppu-D76n^HzrSN)wylHXSLtlToZc>&#o6|HT+54S?0+pz=f4`i`ceo;*6ha>t z^Vto7i|EVY)!oXOWP9raD9&_EIM|rXJ+y$&i`FG!fvfKe_h?J%dmNKEEp-EbZ8Rp9 znZM2^MX%odm#N!hh&6t5Zl-NeepH-oo$$khKJV0w7QrN3Y;vzZrApxz#?`T~wXQ<@ ztG?&k*FE~@@9db_8an&g#g4_I|A0};-W7D$*;W!TE|qE|TjJJ2XK2yHHt9_UpxSP> zCDJ~I17G6fPB%c~sP;}#EKFN(zXln)$Rvf>H$_kL<3h-NwPNGY{+_9qSR{wj#wF$`Yhdd$h`soEA;A6VXlOOPO+nHYjH7Qj|r5+B& zhM;|q$E*SJE`=G9mPIcI_fcu?AGqCag6_8jJACm^$8=#NW{Y8?;h6tdaS@;owMU3< zt#RtPIx8~;tSHXDVU>s5P;X)6V+ddT{(Ojev)(@X*+aX_3qNp4C3%(n6&O+yT4AfF zEHdQR*d&04Am&@sdw!0kz8>Coq$T`_WaEDzEm74hQT34kw(~b#Q^b0iH5b&|MQ^v_jw!XS|dLnj$7E5Smu!ejz4t}%h?nINNm=&F0Pi15#( zLoVZ@$S^UyLvBE9(e&<7h>3}*a$SGurb3#coZIGKXM8RauppR#V%cH9hRYBvB&WeB z`7}WG7}0g%@ywd5fEd_yr}@kn@E0+l`~I{G`1Lp?vs^7R1Ckyc+|Lwf-;~YM@=*`D z#I4S$5TP&is|F4^wu!129CUz&gU-KJu(LT^`p@aFtQFS2oxuoogua2H3&9|A(COHJ zDHtLG`nW+KXle(-t9jzL)cJt4Ug;U8w2Yo?G4(7HxJ+V z_`Lqa<&=~stWzk4^?sjv8C3n%*!bJg{Nt>M8dg#7{QSn1TT&>$pnnp&V2~2``@)_5 zUENoiF|h%c``OB<T_@tlwC1N%m@hh^H-Q_uXdx39sL@_}!Zyf~Fn)?5Y<0zZ{CoZe~!!;6%ul z>keG+q*W|ZPVWPyzR#3uT$Rmbp@o-oP!ZD>aO(_6I8@Zhg1Oa*ZPY@BiIoRzBsOHl zzQhZ^Pk;npo_uo3%@8?AEe z8jbRjA^u3Ma&;Nscw+*y(;7-kn6j+#GLJHjvICbRnEC^PRZZ1K;P@KrvO8`SH?ZSn z$)%;OUAStWOpToHAZXEZ5t+K{@No5<(sdvIgel7a2>ty$eL6Wk2bABYkFMgLMFd~( zE>)=&$rs*7xny)|0x`HXqf-l^&zC2JAX1JGoSdNz+;giJdbP7*WqDh`?&UmV8sEk^ zrzmWv8%h1YAWVRUkC>PrPV#$=bDX8mOBJu7+9g+A<6B}xgCX38SA-7R7Op~_ov|EZ z=0asv)$6R|Dxs64Sb4bY_xX_K>4GUoZ3F!tH@%P?zmlZTUwY(lsj~3js0m9pr@IMh zOx}fwd9T`^L^6_Iv^lh82Gb6CSTl1=HO?9|>$Yp^*Z}&3M@4h5KNCoTs*?woae0r@ zKFLyX5vtwM%fctr8zf1-kFw1#Nf7kDL}GyBk4Dhb(kfAQAcsRkNRos~)+~<}A;G7a zQB0~i4xv!%94SvQcL*L9gDK)v6BU@EC5wYJ${QigTBE#KpeRhyf`-9HTkloYY+aVt zjs$vn%h^w9c-B@E45^X2jL~}XYG;J+BB#}e@kf&`1%z-+4oJ$q=Ug4Z_1WB*&4NpXv$!vm*Gb{-^rGOq{5rs&E9jz8@gdrKC9FmZvWn=Qr zU*mK>O@a++vM*4hB6JI6>)-&buNQ|d%Et%WeI!QzK+PdS?Bd{;`(F6&zk2%dG$8+l zle~N5e|iH_+>O>6iO|x-r84-?{n8BYZg{ON9Em1mQSt0--DKOxOmmrW*#79kyCBhP z30-$|VmCh^jPOM&_UnNVRM$f{jJaVh`z$1#%MRe?7Dh1-rg3G)YDP?lJ^lM{hp2~PgvaJ3= z7|n<4r*#h@&=d07uaQS$-#wCwO6$?}mV=nN@DqM!GBM1H{Pfm5V2-P4H{>U)7Kv2T zQ;DeTN;U^XkXBboLNFEp1fkXUMqnMmZt6H}_6Kf=ye1FQomz!eRVf)-*<8EwYDvBR zbmL5h&^x1@Y(($+^?hba$lN#Y!v~Zw^6dhx-SczK&ONNqDSBs))z@k#zorKircZA^ zpi{NW!VkGSlHf03(z;68bvxeXImZY{bnNW`1r#tD&Ea*CV4lH0I&>m98@cS@1R9?dydZOT=j8QF zt`7_#ytg1lTSnn^HqKgRJre2=r--vKY&!fOuQn$*r$+w&x2c)sH5FpCq8oN$s**CY zs?$;iKV#1V(s8K5Z<*#R&r`(*CT5%e^zExU6H<;(|8n(F!|GUuR1GG7t!IX zIl_%zC8g}AQc}|ZL#p~oWQzHXBvyRv!4{qXqTTWpq9)xSG$3740B)GEq# z7%!v2#BxXdE!XgCk5U|Pt7So?pl+Qw2*rVe+8XRpM{f4b zRAanBD=T*5TxagD?+S}m@G%k?X`xlaYdY*$jp)uc znb0OojX!_K5GR^%@D28cXNKU8DXzRjkLDeW1n}yp<#*QK`Hx8@|I*#gh?V)pw>mEK>#|tdmG){&xNuh8!YkYx2QV_z{j@h73Qi@SbY=I1Jy^=a&Ub2=MizO=W zlh;8RMS+%^L1sZJplj(7=}m%BS)0`~v%&`IIIc#sjazPKD=Gyn+sxKF!F3vq?q0%g z{{6d>;XYlB-Q)J@#u@etM(hPGwbB>(WMJKLV9?6*X!LpRV$!k%tdgFAP>AzrEZQX8 zedC)XUZ^fEOt9I;sv;|XCd}vTlz8GZR};5>P}x&+hxEF7%Saomu=~Of=9llz{(BLI zZU7b~kXXo5wBhZREXE4eu-;A@eZMA-$$$?9l(w9RRcq#>H~c%sllLaHI~qlyi_~ah zf)l_z@o}E5Gy=Yq4Q4hJp&1s;?gv2&yZwVA;xhe_J1R?v9lY!+S?u?tASP7}HZsT_|d7U4IC6OqmfAoZWQTwoZlUK*OcmKR&qvlK`|30Gw%Dt`98EAonkd{nLsND% zU}_Rl|1!@ZH{Mx#r=j5xL&o$@B;XYzF00Qfsg6xAeW{7=E7xUxei3N`2{qerg*J6! zx^moLYJVw3Iar;AJEBzb8fH*t?BpJ)%x)BI|MQ zH}MOtoi?`HK&U=4p}~WoLA5&M*D#Cs$^+e+wNj@%`bvUn$pXp-0KhlRZtA4blE=n$ zYv~v!PW(IM!uoIwQ@e&CH%bFc|Cs|%rdUc;gJL^1nx>q*n2wR~DoprV$HqoSkZEvi zo~b@fbpVTEddXs-0kePB2Ct!w7djT(;$<4C%Zo3Gz=`X8Gp2d4s@2;7D*(iZ{>KHN z?63PAfiVP+G^ta%NnjOF;+J=iCS=|nQy0QF;$7TaS8tdCJljT~<;22{-F}YiNk`RrR zT+Qj+7(z06sXjqg=a#*_Emygf(UF)9Xy2u^7w%}fa+s`l?5jRqFO22r=-NVhE>zo? z0hR2nC<8i%H0{FCT7mxQpreZUGu!xUgK+CPDUJ87t7^aE;g|)!Mm?#q1@+gl1d$hT zP#C=~t29ia=PDJ%W@qC0^@DGFCQ*?$j`b=cE8$>N5(DS1_>qhU_Z9Z)a+H4S;ig~3+R(rgb3@LaNipkI0|LS#E0LhFvj39P22y-+b8kg$d zH9_RiE>bgJtW#Cj(jqf=;d!zku!m|;_uQ4;z zMi~>5Fbr2&&*6T z8hCyEI)}^x$RcPlV!wc*&;akp9HR(E`y^+7Fl8BPcRwcKct~AY>JYMjdJ{kmG(MSa z3FZ|yG(ATK7m!r_z*(CI;pE2pKwY}!LWtK;PH@#dXIE< zV!VkOm5MoF7+Joc1qgBjy%H)Q$5dHU18r*?HE9Uj0x>f)0|LWiO`TUP{?CA8z`)Hd z0kI{tHOdddJX)kuZltY|_3RaC#gip*(Vz`fX9>vxClqelyZOj7_@hRkjK`4T@)^iV zW-6*Cbi5wj(GmCP*LN_NU1NLj$!Tj$nTpeV^AEJJ(Sa1yL#&Uq1ap!egNk1~#6>E* z|L?0txF5jWRSw}h%xf9s%^_~dNX-|iOkL5Da|vMygoQM_5jus0?@&+pr=_J?t+Wnb z^iUltq*AK^g|iYT!fcF-cq|?=I44c4#s+q~`_wvb`4!8`7Bz^xyN1tHE~=URYQJGOfqQRG5~JO{G*GW_Wkj6=e^IyxT9j3FjYCGq1W)+MV|Oc$C5w8AFyLd!zo(WNEd5BC*2A8S z7gS2RxVD!UERUi|Gjg$Qve3TYqkU3Y=jf)+eii^kX*1(eBUfZNNTvw^fym33$x5Hh ztEcsTn(#BJjHNhBpK@j@WjeIyf}=@Ydqu|>SA@}?rS_*Nt^H~)mHq2KInZu5HH3KF zj=c#j9Ybnmjz%+V+8}YMfgXYnSqhTPMOteckeSjip4=AJdIXZ8*cJ7gEz6v1#WvPO zVoI|7^tdwT+DBLX^xA0jWN#_ZiReIN6tkUv_2a3gDEebMgluJ@@}>kbG-}aPPO0EE z+pe2{r;aJckC;qkyu?5b&bDsg*Pt?Jaew!TSfv8&loA~s-F^kreK=3=5?qt)dW9IJ zmEORnvFPTk9>OFI50H8mdHE+JG}+jOZdbBj`t&Ib_NWic1FsY!)ps&!O<&z)o=iSA zC>R+kU{k39xvX#41dam;lg)zvI-h%;g6Ks4cQo1w(YiZ^mwF39i3#d>O(T62xXss` z-)zEJ8d(dPcc@9H;YT60kk4kk5z&^L{C=*~^*>j>5TEEBgXCE>J+byb zo5QPQDcsB|-J-=}S4dvI@q%{)?2 zK?Jd&4f;-hE3=;Z2IKcXohW^d*6o-z1_~o*8ABU&0xa*?=;D&sWVY}9jopOa0T6O# zK4Xk9IXYIhvjr@Kx66~CA%uiFY{!G~7+=+Mkf89TzGkJr3?+Qd@2HA!cp!g=A4QE^!1SXQ-uz;8xZci!QiiiekzmiDs(69R0+2#5=yOrEWrT_TzmWpHZ8bo zw3E55-SSTV#Jzi;OfZo|UzI?4^iku1smgux5b%k+`pjU1fd6vW3^o>Stf&O*ASK9t7I~t_wif#Y)C?-PIJk~WaK9nnUdLUvA% z>YB}f?lhSaA4=X)+A-*s1|<*;9C+K$LV&Dicl`?Gn@u`PqoVT4p*;c;H8L`wmtpgT z44kA^Kjr<$mJYvmus*20tGk>MLxo6R$=$5YC!c>p{}5N|JCrIYL*?PiTQK*&q zMXsMxB`WpWvtn>U)PfgQR|hTxnMZq_i1nI8aR#I_tM}GAe8ShiVQ)K9!r%|1-Ei-9 z$4t{a)oJl-oX<9>;gW(u6$thg5H`NO#gvm;BnnAGS-QabZ7q3A3^=&Qi+Fx^0@~rw zis3F*EA55q==w^^t()0}i8Hl0WWkW`21B&p8T9JT7OClcnrJ>EF-m4Y0?zm#3UrHF zX%Za+j@*e86W6=8VFN-^w6IDlK)=$Dapk>!K7&v_1&Y+Q{gX&djmqd0Oo10Oc`J~`ep`1OdK}nxr~Xn;fqYjw>Mg2aI6_D_T13< z+pP+k8;$HwIy5)16XfP54JO{nZ=!Moy(ZXIIBE6!h;NAlTJ6&f`X(^l;~f9Ssa{KO zhI~8R&}35G;?GsM`?_1Y!z71_4y9$V#@?ZnIDqK=$XK~9qSN@%Mjpy+ujQYpINJ%% zM%~yZR7Hx%WdUhp*TG3MVCb0h$}ok=h^Aih_qZ`hK&+-C;SboWxPyBNJoDza``E>8 zz7$UZFgt*b?7Q;g^D1DdaO`kNR0fN~QNYdVG=MUO%rs8PrUW}ESED1Ss<%#-E9hfH zQ#2$@x%=iQpQi+5HmrpKMKVkX8a}W%S!>tknNz}SkPA<=s4tq_t4yIc7I~F@P?@z= zn&s+9@`Fyp>oNRu>I$Z0*$W~Q9gPvBY2XzK*o#Ex=8~)hJU?=r9qf)}uXOk{VL+0o zHvpAO->2KX0k`R;CBhd~WQ~f#6j3Cga@zu?;YKbl*n!p!BOYD%`gnMFmJK?9ca?Ap z1|`f%+t3i8ocs?$4`Z6}FG`D~;L4%CO+AE#bkoSjGtqoQRY|PS#%jr8j_c_BNa6Up zenI4wkL~MPpTM7H$ZgHF_&P;cu`rY_9SYdglYoo3f&4h6))sX=53hEbVUX%s(hd0= zM(ciAiQ8Gs8S)UXnq^{?o40SI7e0FwP26p3on_Vv7|aPda6QB^IUk;iRmKmB9Fvlb z)WFKrUeOy0{rj8Hp&GjFwg+^C#Nohhhh6{x1ApXpFTRTQ3DgDn1Ypup-=A2nG13>A7hZZY* zMx6+h1*r3UnZmXabtH7ru8VWWUT#%a5>%rO(7sP>0ti642<^H{O|1mWu+0o zekTf+)#h<}zGPD~b?p8!bk5s5>p1m`cW)abdz>P}9O3}5H{lD5Cp3rlGlULjchoUS-eA%GqCEmVhqy_0 zsmX!?y9G}Rp)^7EeDy#K{m9Zw^uzEI4Se8pa>|z%V zQr#=O=CI+Vi{`lHvi5zjLNO9$xTILJC`pXLs#3SQue^lH-mThNG}OxkPnIOW0|0J< z3IMJ3Z6tufU=9`&E2{(X>(eq~*&bvdpB90>DDZP0b5el&8~?PNFoAKLung}|<@+L_ z?z+d}cFQj^Q<`IrwA-+4ZH{lrIZZ&hCOSFWiji>bCNqYaY0d|wy*C-d=^IH5TD)}9 zEr9Q55oymx|0w!`9_OHmA_-3PU9@kum}yhXaPq{S-b?5i0uUQQ@TQsP-4izwhP1(A0w zJKNdC0!@%bzMo{*I-(Kap#Zp*rQF|~Zh4Wa$Yzbvs>qh4%pSSvRKZdmO)~wpUQ!MR z#{22hEtw`ff+3jMZ)(aniYD4@qOA6I7rsS`>LZ-<+RRrQ)(Xwdgv`_Q9sV@fEsbHi zu5S1rmS!1A`M+gkWLzRpQ@ChOz4j4mR4(HB_gBi+w`*`uZWC@fQ{NQ*PDICu9+&jo z-he7GvK|96!9Ns^x;sFd#?aOkg_w%Uj8atM0@u_fHwdjc`w@ih5$Xh9F<`UaJmD~9h@Q!uh#a$i&!Gw zX8%WTO#SBO=AVFBlV`a^Wq42D#TwiTL+@ANA6SqwWVN}0^?mv?nnM;z9`vKsZ=Fs4 zH(I~+^u~S-%o!yDGx`*)^&eZZ=~|`S%UPDM3L-F(D8mLUKGBL}ek=))^g&FsGhXD$ zI^e2NMmAOJkR~zTVd%d^!)BA%fK7)IRZxefrg32Xs+V3hY}sIP3pRTlqN7lAPmeU1 zqmq1jT%LE3cZXt5b>Wt56(>qwFKNMBVRrgQjfG!$*UXqHpc7~)M@II?-_E-Y4I+wk z-`(9UaR1bHv{bhyKvxv+Z~ZCp038%ZH1K0hR`Jj;1*Z*>70~Pudmb;BNwpx6& zN)c~!G|dv3PNP#f_EGCY{K*L}r)3axqaXE+fd>wql)Iab*7CwxPo`7+2Ut-7WQ&(D zr`u$YDS-)q0o`@XFH!MDLb=BOis=1sn(X=C5Mt{p zJ|5n`agWYEz|BOyVlJ!2uF(!K0|V%SJRYZOV3qP(z)%ACnO<{sFMG}h3D5g$yC8CM zq&I+_)c(zV`^C6`FJK+_=lEFvj%XKVsJ$oGDfs_}!1QemM%30HzNc-pFOKzBt$t7r z9qtZi3IBZ6>~Ts;{bv5hZyl>shVV0~SE#6&EpE=nOkb6lJ-C=NbPR^y4Bt9abAa7a$~igkL$*on?CtVbAL2< zycTj{HJSc1InX!G7f|iqrC4krJe$NtiJdjQGZ;yVtXTYBhu@BZFs?=bO$hv3FVWixrSvxA)s_5E45EY^UDS`J)`quZ>1 zFurp8$RYxaqNCVibFhHJINEAM)^b0*Txsqd{Hihk4}876(-+jrQ(Mc>kWL0GPtZ%H zxf+0{Ws)$$j4`SH0rJGaO{s~aHR`YcgKB^Qw$S*6c*wE}_}v5GZN~&h8_3r6ki^zS zjIXOVzZ1*5W(2rqosh&f=F;*4ELf7|#>IsN;L@x0+XmnJ|9$(Sjqh6e&rBQd~7ld!y zMhlh(Q3O+@_v>OMx0J_9ahfv|XhOw{%amBI+f?Lv?|!xY`#7)7^yO_!8FwqP9}FUI z61yV}hhc_Z3&QTb>v&S@I(<+s#?+C=1&z49AGwGV&MYG4$grWc;v8$1<~jNhsKJIo z=ZVnn*i{XQ$UEEeRMM}SEDaHlfMbA@MkXX)mW5|g&Qkg4PQu>#p*UDCj%xy&9H=-F zFO_QMTLPYi0ADJ>6`?$g@M=!dCN_=gS^ZLzCoAKkw}zwuR>99=i*r4O`owGW^3Hr)I+RsrM?Et_-rwR+MXW`a>APi94dpmzW!c?PFuJ&(O(Vz2%pS&x1`a;nE zJUJgYSq#xy2&Ctx>)FpB;L*+hZYE!0XKB4FAYZ`a^p5iHM59%9>eojitrrl`?G@@NGn=FQg0MAq13xAq2TQ zE5k_zCqMZ3f@A7AzDZ9AyLf17|70ZLEqP;?y1SBJzGjRFszpEAZI%8< zBQ@(^VqcN8Dr?y*Zm|gqp$BdB^2=B0*uyX*eqW61!1xivLmTDdefk~?`V6O$R1NOV^ zT7LO*|3iUT_{RhMvJJBWX8Tms#pKO2%*JXw{0&V5TAoJJDJzdD(#zn$Rfi?O!iv!5 zDdWfz@8a4xgRdm)vJPExYi1&w4ZrKvN)i;cyU~yG4O|gMzccZVg@2w465B>CnkrLt zkyEuCT+}i7P1aS?NQ3p%68Y52O}(M&cMhEvI>#<6ti415KO;ny{fpjnkM_TbYVW!P zYwU~7#*eNC-Hi}1NOq0x@1UcSBT{Wp((4uU>=xgG#@@5MX_fc5eE($mtea~((x+9hZ4 zefM-7&UPO?YTGZR2AzmQFdhJ3mu={TQ1Plg_R!fct;FGt>Z5bO0wg^&6#m7n3#iq- zw~t$GE7$e*Hhyn-IsC8GPSoB^JAk_JH8WczG&)dl&Ei>GVk(~zS7x(T?_hm=HZPf> zWWM_vNy*7BVQTeW0xt7yrF7a`_XU&e8qDUhUo<<1jG`rL^jx4K1VUT$wr`b@8-%gk zJLtOOyzwD{;y0*;6TMR8wGeG9e^jYC5hT}gwu~af2bTCz>uyY+kYg!!X#zCUw5*|d zL4_32U;Midgp-WNUTLVBmV$+uU_;yvsBF#X_OZoCU&Wl$)Fm@0DZ(uW75dmb!pee8 zVhM`S&VRnZtAAmvTWx^Q{b>yboDK!YLMw}}CxE^YI9u_l0+Zq0gy`&WsARe)*%=TqxT@Hw2L z6@GjcGV-v%5Ba=6zI~}*&xloK?Eb_cvUGT^mL%(Yb;+c~e_92EErdP^(=H)h$%U-3 z=p2NcS%&^-*1dWKvtGvt*qc=P);^x;sx1v)SLZ|_sbaeiaIX#AbTX!1>0lok5W1AX zL-xTPwRNDb*Sa(tWjLC-?;0}kIA|p*9~wgAL*x62Tq=;^u!steBIIX_>d}VJU2T8Q zc5WAG%82Li$pu(Tf1ja@(NCEU9PJzO!y`hEjvaNIxx&F7=Xj?zJpxyCJLHl^iY>AR2)uoJRT!Vs;Q+^?)A zD9bPt(S!Q8&l31=hNUA`!5zQPwtcwZ7VCcLrwW@(q4L6pZV1WRzhnN# z+>miH1g@uC^r!sglAD%^6Eapzdp^N?@WfWYxX)cr7%;6um! z!RY|fYbFORTNbh9fFM{FV08Rr`JWk&PgJE?Xd#wLvheEobZ4iT}Gows&-2q?739 z%As5(tJ4|-2yStn){dLiS6UVD7PTl>#qU<{;8zOpEh`}xdKr{LrnlG43LD;ML6&}7zBLBuCY7bVw%10ORG^Y>LLo31XJ z|5C{xI)gD$hE#sFkH(|!Icf|1#)!2qM}WpR+--7Us8{?rN~^*B`ZZ7<|E?1Dcp;*{ z2jk@JL4XVmxWEDDZTNVo*?w^?pq1Y_eDVB!NFZPge75@OuH*HwaO*H{z$tCb(-><) zsi)xWeN%)EIwfCsoyIV95CKTk&zjJ${|Pw`#G3uoHM zr{^2O`;^;QZTWna&nNf~e@@XBAB%aqo_K#$cI{zYZO}dIb_+i^@OWN12n`qn3V0ao z7(RITgsd_K>^RK*#iL%%A9@(=Qa83cz_WRtQq6M^Ha|7ZHhetBd+NxT>$*fUwpt?N z@w$$8_{}Un+co!T&tuOLc+(YcPiMJbf{!ttnenK%QEa4+5wUJMrc=b;(7?qf4NXle zXt2#`3053K=kzhKYEKCjWRL-W`NRR z#&-sp`=9%q_>TVf`X#JbSvwskD)fH6k^fTwd=z?E{exbHKgeRA_S9SR=UOi2y9zG~ zME=H3?C#PE-!HP}?NNjn0bm0FFVr_<0)y={hk)m|LeE3I<*NwKi0`cs{pyR$pUG*x z0z&tmPf-k4o`SA}2|L&{ns3R3Xw=jjpAWF2pRZgDTHd{XHjZ7=6uRPVtM~5=l>B?R z{*Tbu>c?gI92H@{Uh$Pv=s1~%m7h-}4Drju73=wGxGgnq+2^hBAiA#RK^PgU%T|rx zmf}+lo#5jD%K8(V?jLnB5x91gqb}X8i}jkKt~oISY~L#fF4?e^C&f-$g=ro>UGM+L z1@Lpx6bo|jZG1AinvT>u;+>z776Ixh08&n4PFHOqlXJXk~*JP79A?UV3rQfp(I_R&C;=J&cnsDXXzarP+Y>i62 zZ)wHzXQutRHV5Z@tG_7WsqJ_?x^`1N{85W?rx>@<=#mhU0ADTWSj`86#c!^`-Nt7p^YZ;2jB9OA15^xjA2- zQXhjaQ4TFnoN_5p=A)HmVUiX~Pt21j;iIA_LHTv2K5Z8~zj_5K*4e+#Q)|m_=}%T_ z^_vLxK;wz<)!LBYK`?-(1LV~XcfM#)s;(I9`J%=VkCOjA;b(7_rPG_}JgqTv7e<-F zD=vx!EWKyBiGyoN6Gg36QJy1L;i{=MpWYc^E2CTFB3jSut{Uep;pN_d;}0_Hlpp-F zH!e_sYrYh!P!8x2fI)xESZ#G4CiptO4RmOQ>xfhQa7bm;8L<|33RCkDF>~g`VK!T05NJzQGH0#htqn%6>j1w5Cr?9d&tk z;rMqF1OXm(kK>OF`d%kilV88JIa(eK=BPF|SJ6&xh1h`(){VA3CWkL{#>#e2@{|5d zbI;j;yd>g@)b2$lQ2~8O7K2tUp7kRxaa(xrn~Ar%u-xmA-wj<4!L9O~|F* z5#QFIrTvC2s@_)u3P>sHC1VF55u4a-YM=_%t5SK*)OYik_}TrHG0vH%X>^okPor%S zHHEyId;bu)U)$;1#md*XEgTcT;pIQ1buU>_h;P-#YLW_Mm@vreo=sEJ>ZA z0Y2?o6eOWg3{3mxvcj+b>VsTyeBR@|)_Pp~_^Q%6NIx3+W_rDA!#s3%`S>gq$uSF4 z)YAT`VNdW@*ggS``9T-^$QAcQMY?{kKuqTEPbqJdHX(l!lX(+nVTu)Zk?$R?X#ve2 zd14a?+cL-TkNMQ94f9-j+Sc?td;jkW8vo!+cz#&bN!QooM@0xuu*5K7`KCfB*Z10> zIB4c@j@zO9LQg3aMyYbfI0NO}o0V?H0`K5h`FT@Id^sRp3`sVV`NoP&$3Hh&C*89c zZVyY$YN>m>$2d5K@l(TLZXa>-_&ay0LDOjL9QNTO&Xpu0zkI&HqOhy^0~R?oIlQ>v z_TV2O9e_T0qe}6svHVtOxlN!<=Ur^UZp{4fH2?WNUyPo%DAJ0GoxQDQQACeXAS??& zUaQ=N8vB?uuLO<_L}~da9hLcpCpr(J%|NK=SS{q(4YcB{1QKY2%C4In!Fjx0R%QR? zQ+}L%ad~8kS84eL4W9>+zFHQh7TnMY-8?UcZ_TJby^hw}8*$OHaKqibKWLCFB+(+$6X5U|)5@exZii>Nrj?P(Wx`-n?|<=` zN{4>aLguFAL0$C31ZgG4@3UT)QnlBt475a zA>PH`#0x0ly;?jC4Aw#^Rh7;&K1;^sRLa0ykM+LOB?Z+d!X8rE)Rbm);`8zLdSRW` zV#+`874Tfny>OrY^w%L^QMhfaOv@ISzQ#Ui zXOzF!Lzen}M10aTKI)eD4}3%8yiAS!Uo++ZO$dbU_cFjeT@*D0+2deC9>$-i%`Tvcb^wD+1I!;&*!n#22NlPOCv20cy% z*S1V=R`@?>V3ny^j@+A)n0{ar&l&^Gxy1B-0G&*aN>u1|XPI4U>*+Dq2c4VRCpi*_ zhlYl#F6KUh+z|kps(=y_zl>9V5+1Ox#-62LDfrH*WYH23jo6EO-|{NB9!`0oqaO#abYA;2gW;JAqJ@QVUOOiz1Uq{T?|iR7p`ou`p3!!9!+I`qbZfpW?8JBB*&8AmPG;#9o^yDV!lnkf2SH4mcv-+!mZZ=GrPKwld6 zIN2%CNGWORRuwOzoQD~o_shj8z^QzDDnqamOK!Gu)~F(roGlPA{BBz%t-y4!wAOYBsqud_n)mo$N{au&5MIrm-0XjN=S}@7ddcQf2-vD~Zg)|*rN>rdYUzsUao%OBjCH1_&n`rcy4H3gP7 zit=H8Q986fA%%fHh#f$}ug=fA*mEZ znVzCN%7<4;%aLjYL>b+wW93xeP>d)CV{5Mr?Vwm29{cAH3hid)aQVeeA&N?uz^sBi zr%su%p|p2Ksp*MBNZ0p%)(uPj=iD+$lczTTO^Hr1FK5A@G|e>sz;HmTSr*)TIKs#@ z%H#rHd7Nw4*w_>kXx4mA%;P-s(qjSG#t#4k^#lOT+o^c}{?mu?Q%69l-2LgvrhU^weyz5cz>> zpk^a_HmTCG0It0?f41eicsN)8ptI@$Q>v!V~l*etC`)yAE>W)7Ae+)muPS)kR&Pf|P_H-HL!9jijWY zfFRwSUOK#VBi$fMcO%{1EiE0FP>?R^zPx??{{OxI4Tm^nz`5t_v-gU*=9-Ji!>0=4 z&7y+IY1?ha;c+B$Xf}@bn$O%i+{Hi)+jC3jJd znpQe_`Qq-T4@^MRSW;V>zNmC?9IF-7RThkW_avXrn3W*+qf!L~^J~^nfW?pVr$N}{ zbz)r{^iMbpDZb)7@?%lff0Ru>@d}dMudHin+BifXJjn(A^q(q$8I)$G>rY~!t#H~` z*{^oD$_~nkN{YNiI^<`AuR)=#*nju`3UlpMzK1~s7 zks;c|>aUH@gEu)pSk$UqiKm1f>KUE6^l9B^237GW4H^F_MWeOjcJH6!Pk3wTJH+SR z7raVu+xs}4%KrBlyK+_TELiy#5k> zi&yUDqdOjJQ|(;IqnF3BeQjougF~hM9<=9Qw1u?W{lzh8dh|V%duP_PpHNTThaX%( zofq!)sp@r^Pqvoda&&_6#G|vjPJNbZj>yrS5~GsBIvwmqu1TNf!+A);nJ>c!JS7vtIlLBHQ*`CBA6yPJCK zC%t9aI@1tZWoXKC%ES}&SZ>tRdbtBFStLX}$9P_BzGqvc(%faL3R~Q3k8^CSXzI$k zx;`1NxR|)-1^4fKV~$aFc@oqU)fMLB0_N|xc=vT6T|py%PWj8dA%239u$mh-&k}FD z;M^Qi4@h9Hm0`TG%%;G$P>D5byy?!0R<>uOT!ro%222lb*;$Q6&5W~(!HlZ~xlZ(F_x?ejo6(>E<*u9h7U3_nz=E6t=0|*TC zGMyo5hV*yTAESd~N1ZuR*PJS|@x(EXp2PN2S$P9>`tK(2YP(l5P7~d&h=?>88F;=b znvkNU&l;5o$TzCy&-U2-QnKLvl1CdYs!jYN&&TK7rL#)*=P#$GSeeldFXxrlpYnK% zDvx7inQVL$OT~)!MAGUc7aduaF2-y$N~-_bnl!fwnwPmS5;5Q$cu-5htXXpI*Ec;X@5;?di*8li!H6M^!} z6S%*2rU_&fNdC50hg7zW`_hPbCGf%L0Q~+lkEgy8E9Z-%SM8&YBnLH9w)w?7^+T){ z<>BGszYp(EAl?^G>l&xcPlDmPaogJ#B3FM1Ml(beYPEa0bMj4gu0ZTaIe`^j-(0x1<$o+2Y?RlVG+ z9y1@#&1eUlY9R|{pNehz3hwnGo52Lw%@HORO#t319_;0!v33rFyU`$|1BE>Of$&w|4`9x)c<# zx+J`)R`aNvt=KO`ZI?aD>!)rnp5sbSb%>%5mS6j4$(fW@Dav0^m23Qnqn4*iM<#1s zXNN7hxtf>Oqf{|Pq16pA6@>NuTD!gvIKBBm8U&azfZ9^G-;!rUX{U-`2kX8%_^V5! zP8!6YfAR+hm#iO}NV?MKV}ggmQqJ=9S6BdlYPu7fIEvm00RsL`U^I%VLwQxIr)RX;GcwhxHl#IkuG7 z)?ymQ?^1nsxvidFb8C>l+vN~=B_rL)*gPx3VK`{tUJ9G7#x0~aq z_M|yC2ev<;bPW&7QkzrNY+5n6_Oua7@4lf)ADyhjs48b>-}s1EId+TZ{A{!r&w_PZ>jL`W1n)6I$v6bvlJl;g z@7D#s>Z}c8Oxuu5akIg#*ZsJuYD$|lxS3?rzFzhY8Uo>Gw6PvAqplx|jE-(iG>3gM zI~gQoOsjvk_!ZM;vW&g>c0wR)+Ln{2(V=qSM42h2vG(pb-L0L@JV-|(o=%$l=?t6IAQ)seG}>!=H?qwIx!T`%5`B5O46PUnm zkUwpSAR*tL5AYEi?qe@G1A@t*-56WCj=|Gg>eU;Oc($tB4{NITpT1Qa9K9sO1Z^1D z)+9kRXL2N%sA3#7zC+#izkj11K<(y+fEB0F7YfHr3)c1eJ(Q2h*2lf`y14%Fb4u6< zC+4>o7t77vp(iVHJY|)ahM}g-y{E%;ah4Dmh~4v8PpZ)BV7;5ek(lKS<>NKOHQ|}d zO)LEJw2!CTF=|KZyh`tneXMh71^>M9s;)ie^|P>0yl*LQ88Ji_a2qBNP!HWY?_8Fj z5u_?gsfvIH*9v9Ojc0y4b(%`%hEv+92hKt!`nfb;;5_fl>9`Fv-l0lw1K^`pK1{r1Q32~~1)$jt$Z znx?x;vPRzret9HwC_GrSK)dxl9?C-`(%idk7tCdSI+sO3(zk8+>KNcyP@LvtmRWzD z=wO{BpT=sM;tJ4B5%oGaD;Q5ZpT+TfAs)G=XRy9?HpZA07`;X7cUgZ?jt*B|v3l&AK;aEBg)yOMK zPk5+sN68qBy_LZm#*Hv85YSA5fKPtCWHP|eMWkF+Ao^a|5jB{uoi2{0zSR={(~4; zOmRwez%q)6j8rOAMKetVu>?RW;+ejW_&xiz7A-p3T>++i9XGe%G}rLy^{mO)a!So4 z-omh0d_E!bhjS5qTJ4WN*(sU0Vh4ru;ey|qrRr;d1bTB+6OCF3P6s7#`X;A{h{zrU z14_3oA<M`3&9kO%yXTXO zcY2I=%y^|zSe`%og8^igI8Add(uQ(PmU9u9Ete0o2btQ6Zb@4QJ&wc#PJO`p+|GoV zdGBwL;T6{l3pUA7f$|YH51R^bGOt}KRl2+Hs^2zu2y6B^H)~qC@aEg+c_AxFAD+#e zYGW1&^-Nru`g;K1!kCBAjW<*Us{79{fWJ~NmY|eiVP!SaQrC~+!S=gx~kGjc>a)}(w?hg&cKE@3fe%wmpCQGXz8gs4C zSdQwBodX7`w4IMa#_Nx2+T@B?*SMh!s)k4wq|>_*wUhbGX97X;yy8ryV*M1IMC5ss z4oy1U-LW$Q8Np6RtG&0`tU2eG=-c3$R%bT{`Ck=Sipwe_QH~$1-QCy+eK{|zhIWq`SH6{seH|A!N z1Es} ztX}<)ocHby5yn(&F3oOOe9bP8pp^t0ni`I_e}4DWm+r;Gb6T`1l` z-+)7l1k^uLf&NrBPtdE7)ey@(KMV4AONoU9D*j0E3kztU{Fmoy>Zc_?$BuOG+&8+w z`f%rM(QfE1*DAH5m0^Lp+u9NOHYMD!Pv28mJjKa6hmxL^bIx?goA5Gl%jM*%88 z{f?(T-QC>}uJK!B<`J&jyFKfX*(8^&1fMDEUtno>$^L<$uQ3+dv(32RDrZhlLM z7GRVZqx?JB!oOjE;2J>b|KofXbyMR}nHtONe3U#|xCuI55#H{H9;Xby{ zLE?v}1+zztjq-5aSb2N;{9hHSwkd#T#ZCq0sVx3}59lyp;8q_&81vnGysmaM3 z8?DV?&4I(@pE}?0n3Vs7;ZT_ia0K*zXG+RhuY(6zbKkrW7Ds#!y_6?X(qA$MsUr}J zB%Ra{sidr|3XC6z7yvUi=%cT%Z|dgeHe@1ty_LgSf950B0$uB?>(|xNGJ-%LL;JwL z0P`sf9qA8MHGV|%I7PmoyHKSds1=*0hdPv)Xgq(XY$}q-q-U^T`sk>mft%5+=sD%dC{XpTqT4 z%mr;k6Vdd%;xsL96vT)zA=W)_$o=WYNt?u2eEAf}lep{h(7*#~c z)_RKWoXN_1VZFxgLyMHf^|sK>3Hm;Tj18TYYoHOBRcmTl)C-gnBWA6UscmZ)>dTo% zSgl^>PeZ-LRGf{HERwHm#PG09NlX_6#;Zzd7HmyanwD(Isco9x*i`4`(a+3!?WbD& zFBf1IsMzK5J!HlJ`U(U7g)F%{?cZ5%JbXfU1O&pA!$3*T0|V z=d}>+P5ot7?fY7So&KreYQ-~XgD_qlExnk(GJ9dIW4>XKeS`kdJ6(_aCY+%z4pzWo zJIZJNoDo+;_zy!;%8%`;VyT%?xXpeo8xPsv-Bdzsh!7IBEX7}^+mqZWa@gI!M$-ga z&KImM^+|w`&RQaW6bB2IsC>tJMghp)_Ybw&kf!;bb1%=#pSG3fAKKqvG`9zb7J|t# zHLsWtaDdN89LKq zeXELJ3>*_NLB{ApICQyhao#-W(x-sL*A0T-gIdj6E z>^W%>s=);EGIb}JC5nuH#FTyBeOJ3~$z~KsD%%BZI%T`I9(xpgwqNKTwH5DGHFlX#MEVN8marwjx#K$jH)DnP`%HsSg;SgK3-AI?re zavbCxvlw$NxkgB7IlT}yts&Lw^h+!d-(#NH(HBT(sAWh7~msVRmC1%Z7j~ zt6cNa28hw9Wz52sT!CimvB<^00NMCZtypbgpI7E3aC!s7dn`?w&n-Q+^qDQXYyRet z0v-A%T*s9=PO?MU)>^S2=-FHZgg7RMr@#r6m6um$iUTxYH=glH5C-+nliq);+vhqS zE4`ypxi{+_nHHK~;j1rNrc`~Ihxoanx3f-4GDoQ#u>E^c=|Y7Q0YSLn-}iNv1uPxGYlMNfe|xdnmU0eAi#hBHCKM$*%>8>Uy-E zj!yT57wB|J-!w{oVMMyY`lG+E@AIjyyARUO(^E%y^Dq9#P|D5C6NVQr5d9GVYX`K= z7cX8U3U~#Lk!)z`>8&5H4^3It+B8d1<_dp62k4ISVxxfNuS^9p8DKtt{J?-x+Fb>r z9-neXbom-|w7xJ3bB#%KMw-5OIxn8}rbcR7Mcs}+@4tjvDx;`f6KMcI3tF0*4<_np zg5A!iMpF1r*hl)W9cE|B!v4J~bTC&bCpLT9GV5zV)wlE$%$yLq&`17ks0HuwdJ_TW z@pkuYw$SsEW@d*8Q?uC~wOJcdr{S9u`y2xq(If(LCOulj~_Mg8LCs1F-rDI@L+Onb_X{FBS$9coAmi z`@1zKuLDt6(0XoUZ~qlcK-<3*p^O#&00F9x0zcX9?KI;9A=+1lYa@Of`&Z_PeNhl+ z@sK5zAYTi+Qs=$H?Ix03@(;kX@uR&+ZyA7sPTOeN-D5a2A2?c+EMq5(aWc)xrHldm z*EKV$OmQ`Vy2YUr+j~WHNTmc{8T+_do0I}(IBhH6?AFeH+T8f;Sf#Rc<2J3*s$Z~l z!=h2BezagiekOo>u)+{w(kCee4U>?`!gwvp4tH&b4|T$&(l^98yE*=PWq@cGPeKX4xVf=$jA!YviIS(PKN4Fz;m^IrMJ>|f zwA7N!8f7Arr@Wj!#(^yPRw%gS)%9$9+wO&N z`<_u?0Ux+p=zwe8>0qSo8hL%S(>EZx&*-wnGHFKye@v?mjq`Uqd z3h<>z4D>+T){~KuA>rh&Rn#N{|Lol%Bi$M$K*m z%dA9-FYyFGei0E-07OV&(;LiCP*;n)TYQQJF#?mLz$$;GGxG9Eqmu?0L0=V1He=UG z3+tMB6;tuv;=S{JqYJ@DNcl7fXO7QIr;YM0Hqr>{u$=2Cv6xv=R;-#Py>wSk4~mtQ6^huGLkD;FWch-( zt@>kTzh6kd!ei{}vr1K)C&EMpbIjpD-jx&Ezfgu~H6S<=t?kg^cNc2%$i`2dM_n{& zp>b*kHlSB2n_r;DYcr6)D4o!-b9uBEIX5p8;X0;DKeuH{FH0+0!93;X@<&p%g#`A| zy;l02mTrfN|6BiPf__eb7ViAO*d0Mb5n0Oo(8>CiFtd^~oHt7zW5GrS6`e?;M!U3h z`aO0KzXe;&vSsp%_&Bt;Q7Z2Wxk)j*KK}?eFDakguHlpm7w-f|20O39+Jm-1k*S=Y zsfo?m?iTD|(0bgw)K_N%qMqetjwln*nHDy2a-yXaK9k(rE0N7O_%^o0DHFhG=|ev0 zClPl@vc%NtOhiDp$V#prMup*6rC1=kIKO{%^E8EOmIE6k8(_b+#9$sZi7w zmi@k{JYWtzWz%5)Bm1VY?|*SrltArw4w02h?5uuXJ=WJ~?v5lHinhtcPLvNhD}DZ4 zyg`K7!Jr{#N<)K_FcCWJUM)@8I~OmP*@=Nkib?@tTxbg}B+3hwj$lMi3lat%aLD_F zqmM&z@crh?Gp3S>4mzCTiINX0#nTVuC>h*P6`E&PYv!%QBw_2p(^tWH>uFmgBenR>Oy2=^D z7>UClP8*2f{V_68_eI;LEZO661AUAD?gRB_UpLEAb>=jJ4VNTpsE~oICiib?epOiaS%>q-gOhgSzGQe0+>g~9% zC_Ez(`A|WcK4f?foUV0Di6Z=!~QPVY}t^2 zm=W@u5R-dm_K_b#M09rh*SV>qMJf7nLCJL~u){=}qDKUxNV!>mX4}qr{GR<-xoCi` zpY{-z*_;fNrd+lun9NNv+{Ey&~|b+|=d~jdHWa(}uLx z)Ns`QR+zLo6j+p>&|LY~PvT&4|f5DOS&LI)M`c^UEiEB|PB3EcLB65;r^<8h$zM-i=70 zxKXAA6_A_(L6-f{E(d_*=$}n!6QCi9k^brbQ8d6({CQRy#$-xl{v}8v2|nW+wPu)X zs!!y!+7rs!546nTNow;a!1}5e*V=^CF4+L=p;M_+vxi$wJwD;&PD&}ZZB*vBlK`O} zAMSG9*vwSlVzoX|QWl}~IVmBd#8mOr(1wj$dOyI0(MC%b>V*G#;0GCLClLh#wVwJ;X|#+Pv`l-zkClmsgEfpowoSkpmlKi9Jvu#UsL?-yM|St<_cWP{3tl{p=e9 zAVhDNW!7KS*_HoFB2Hj z0r#$@wYBw-N1Y0DC(ES+C3-&;AlT~Fp+=;MrpZ2;s>?I%*vPRe52|FUI`3Xib`zm!foqzOryP`QF7%t%&R%Q5ZwkbZc~_!E)KJ(C$~ zR(m_Q(4-_X`E;QHuri|eC!%9rU0ro!Zkp#pJ?;~_EQM|O#{B63CX}zbPdpD+(ZZO1 zDlg4}u-G9a^A$QTcCZu% z9V4D$L&JOZ~e`*gTyBGs~Q^KQ}%<%l@i4~ z@2Aqc0k)sh%ruleT2=YsyPjY;pvLww9oW-$lX`<6MN<@oNp###fL(jv+L5|~Lb2|Ye zB%kC~{A`q)rPip~Q)h!sYcpg9wjkYlm!o#Q;E^uXsN2UOH>`gnHFQwR!Hu2GxFc*9IP)ld(+uBN{3=pvF{}wvYDV}01=y#@0^nE<;1DQ1y6L z?qwiE%k6K%d|8~~jI=FK!~wxGNG!RyxGbJ+-6Y0`N`8CCX+#J_KTggg+YT?7#t8F` zjI~_LX`)V>>tvP;9&d!AeZTKR9 z`g%eMdZ5GC*8-rh_z-ki?KgUpJnm`>(45yT=~pvALNR(ats%Zw?fO)F6@G6*xXMVn z_mTy%Oq))m$eii4!4nB*1e?*JOE5N9p`vpE4pdjOQ>q8crR~ko)ZP9~9s+>#uR=Yt zl(Q7KMuufFNtm?a1G6&KONB0=0A2!c+NoqBKxuB<+*_k&$oruH&yz2BnAb_9Q zf~%hVlSD+Epu=}QhPs=96pyMaQz-OcSUh{Lvh5IL1|}vZfU1BHGUQDmv=+okitV}( zYRXgAe`yNiWt=V+;5uGohJZq1l+sVEV!|L~h?a$icmipeynq-M(&5&_zmP(fB4?y&o|8XjNCJVdYY6uNOcVmVEBmvFaz7q zt{px346qP%g)v$z{cAI6z^jyW_FTL=+03|^Q}{Z9dE({s44at{6JU>G5^mTCarrDY9ECdLJ^xx!N%Y>|TLMbg?GCsyxxv0N;LyLH&YrCx%%M|DMk9#T~ z2|Qkj_`6w^!u0kt?$wuaPkK4sNS8?hnE^q2`9Bh1$`rrT&QwJL2qn>h99tL1Fl-&7 zCfWZQ{l`Mog+{P&^p46N0CN#JU)!3k!rKzy&-Y#Nl|VYhum6z^QENgV&xkL`6DRk8 z-2;MR8yF#Gpb`7_92khsKDFD-Kr{2Ls63m05Iz0TQ8xdNl`9Tx+&}zxV=qIa591Js zb^zX&$0Z`r)t-O1vKn$M6s=qsJ7)0&JPnJL-liM165*5nO2?5W9;=OSOBmD)B%qEZ zLuj}QP+ZB!U1GxQ|9cnD9RQ^Wkm>|MT^oGwX3z;#XvHYG#wd-23$Kgx%Ly2c>bkqr z>EAKVp*AfYsNe($M)^kL1rI}`Qjw9)vdomxU)`q_y};y z_3@aa`XO{$1H`#aO<{;^x!R1&VKF;=(@lX0R8jtCHN+`jxU1$UcIyWrR!5&h^z^ML z_a40v!I}lHvtGi71h#!r!|7coY9ZIpn{FM;>J0O!I>;7MxWiJPU9Y1EnVUGy$If7E5ZuUE``7?9g&;m;b@h4Mp(s=oK)m3FK+WW^ z?Z(#ou`vL;_?BJ$5u$uR$PMH3Py#zB=3JcvvLVp!&$l=R5-ot>2IA73ar3=h2jYJp z)@{}aS2eI94x)+L7CKo2GYXZ<_I`(fIDY_vCmsAd-hi!H02D`n&qACIortKYjr~jD z%Z-eD&(6+nz4Hjy*4Ds1gUWyrDX3=-uC5xl0cs*Pkh%h@S`OEz7OVUlpt{viHUtWv zU{;HfkrC1Zkawk|tbvDgLsc!9{0+D+x-G6xK$!?{$Vm^lY3V}M=hFBHSX*2Bd6WsD z%PKNffT`o3p4b#mcU^#H4e|%Q#xY!Gqn{3!5nbF!ARz#CB;Z6qUV3wbj8z$o;8ZYy zyez1d@ZZ+1+}PX%Rv6=GF<0;Vf9s9D_GjQ*%hd)V)i0hl2Jh8jbMy%4c5nP11!$xz z7;|7zs}1ntzP`Q&@E-Z>mJ!z)Tn?9(l1wGAqHqK3!m-cuk(EvNM`_EWyuq(e6?l`>#HgesvxJJ z=;nxEJ*o41Hus9}-;oukG?)=X)dUBa(y7h{_v|vh780=J>zcfo))T5~68iRKSa#N( zARS^4RVD!Z8}MNuP-u+RY}u0_03(DV_GP?eEp*xu2cchD=aNCX3ygP)KD>FSgp%8- z3TP&vW(MkPV*>FI=1Xk0J%6N_3>d2VsA##u3SL% zG}u1j1^ewbG3M0`2Uv2GU#;XYW{;bH+Mb}#&(8<=AY&fjR&ih}$*jYIj4r3zVeyco zjTAd&pvMHPQIdu=S&&$z=Av>Z5sH{d_z;lD(rxSlwi97E+V;UQMG&YTAe8OCpczI} z3meFK`-162ICGUs@^?V&g%Zl2Y6+6;9sJ-*gs8UL3H4FDc&3R9+ z{UZLC3y=b{bpZYSEJLf43Zf7(eI*T9xHT``3Ig`Si&e46Nh` zB|ur6>LNxjb>{}&u@5@#B%TBr8798~CO&T}9!o1u8DBq?wqSDm)6P`ke>5}xuel}C z|84+$;eS4(gJZkpGjiN(7sIXJjRB5DgZmjG!2qXnrK4`2RZUi2t&;bMm6{Z5SZ?eI zmyC7lVvUPj{b|VgmKmKqI_;-UQpnY?S)$AlEv-+STG>(b)f%0dG5FFB?^J1E^aT%<+M!h+vZ@katU!n9I`z^3!uBTg1l2O`EANZ^~h!pFY z#SqkBAsyMvex(D39KpNu%YAFLr-I4F>b=q+)%_l$6k1Z@USZeaAmDO9S>Gu9Uj!mf zNrp~s%rl$9uQH|{O0O}F1-(?WQfvAELWsxlhd0r!s~U*MU*-QP`QwJbCz!|^$HzX> z=VG;FQVZrk!}j&i>>tYzY08@$u5mO zD+3oRg-gHBDMUZ~nIWql9skAFrv302dQ1CIgm<_CwRdpBrPX5EH7Y*s8=`}1=hchC zjLY_h%T5I@Q6E3V0D+ToxxZE>qSD2#31P{``lya?SV#L1*zowVO!G9z2tU8}qFK}U#G0Ds zv9jiOht1miK>7X0Iniyh7k|p3$A6^r<=fv8N+5a{fL2;&t*N(nItT;+WIM_A?Y8M! zdLzx{s*AzL|EbWGfs&&xeBQsklOX|)WQQh-O-g6l5n1npSuEJ)VsNu^*=+}ZcHeh< z=-Ph99SV0^x(;n0TKaJR|4 zVYEF2Zt20hP5A$!a)s@Vw=5tK?m1%kFG2+1md?@yaui?p{H{-6-_&82!Y$JZe5U@B zz(u_^g~Stt2}Ar9gNCLUT;G4^Suy@|B0A`i;Y08_W>t`o!RMR7gpWV-QoG#UpF=!; z=G2z6oJES8i2LzdrBQGAWU3xKy74G4V|6#<&MW`mUiv`)&g1(L`8Z#Df&rEft?0(o zkO5?SuiSojapj%YcQ?FS3QJ}KQ}~6|@Co_@SxKMNX@dLsW;5@oE*G}rk%RJywyLhv z+NW#&!SxR+k~W4Jd_rM%aF$)#S+~E2u`q&rL3Qr8rw31eC&Mv5Ti)Qj*l3ULt#0oM z6%<9*-oMa!*5hy_6n%7b^grgrKEj9{QRU*t(E6bLFIdd^UgI4znQ@0oX}3{GO+qxY z9Z#I-dMV`_nOX2Nt>;MoCt0Jkv9m>#S3s7HEYCnImgvCzm`^gnZ2Nngb(}Z9)eex7 z&q75rry3%j;OZx8>ov<**A2J(-0(0BUIYTM~mwP-g-em;4 z4M~;$O+m})K?c>_XoL)%WWqZMbK0)=nBV0J2x56TYvexiyms0xfAA~XI*rz>hwaN3 z(kBZTiVJUgb&A(V=IA+_=}be^pKS0VrOU9W8sqD9s8#|9V#;FtW*+gA?4H+E{) zOG=>a;baht=eN#HLzJRv^xw%)%p24-bKO0B7${D`NgH`@n9@quYEz|xK|V5bXTdF# zLYR{CfJOJV9tK7dU`ps~)sFBpr%_ec!}p_t2cuq|RQz+=QN^@sq4ej`ul{I&&Yl0h zML`Qy43*Q=gxMQ>tbGvpgydL;wX};B78vLh@k7D_i;L-r4z&eoEn-ccC~nE0E>JVo z>{tuuFTJyPNL$C*jj!EHm){O)p0hOyf~XoBNwQTgEmz4IJB~{RklB{LB3R`>)I$Hi zup%~#o@DG533G&aW0?0-()d85!Cj{Fc#x)nhYi3pz8wCU_mirC$KB-Dc@H+mBK`1> z-Uxv2l1keVCTnr`TwU=kJ#kDUcZHm_ zd#i~$hjGHhH9dYcEX8ZuXIQPPO91Tj2fZ_7V0U4wnrwR#t51|uOTSbfa557oXv z9y0Smg;fGQWl(KfP1raZa)@AU@t0wdj9-ytmNS1GB*9$W9vZ_RJH!5vqvD_9J9_|C31E1uro=WGjiZiQLj1d$v}I?syQIi>A>|6xcB2O z+`FH+I3AD2tj!B0>S4Vc*k9?~j{A%atjjuH$zr9^nY$_UP*<>E^+P|x)BY_8p2sss z^^>r?h9*qReEr#2+NTiG2xo(}b2As$i#>&()Qz1kD_2@=yLs(zfNKBh*b&aA-SMxp z_OYRF_pj^i#<&dN^77-T&~}+=w6~06>Ai-wj*_1 zb_tfGUOU;go>iwd9X&Pf;f*XjtPnXuPrcvj7h@icY?mLnRJi2ZX*c^lICgY=8htXq z-#i=*=Za|fu~+Kz^z*_XMFd;x$nCbzK;u#IaJ>F{%=MSxTP(?^UVL%aA6iY|M(PUQ z{$tDVm!@`b#-KW_l&cr}F%-JD{`dA6hkcVCpz@B$2OFLD$vvIsil21i2BE~n*MGf7Qn99x z^ZLa0Qw1bNXKZtL+>yr5c=4CL(e#JM=PS+-9{{`bht zq}xmrawlx-wTYJVZ95dRl!vJ*sp+1bdQ@1P{srjz6A_7;Nj|FO(-gk9bK4ZWkL`0$^V?Tsi^^m|qsl>0YbwKKkNX>3 z4{tD@N1uTKyrkGR1zgoK^ni`_RP#Qp@J4iadRnxP!mh1PYF5;^6@Fju;!+v8w3*iL zE})8+eE)7<;$u@lOzWa08He@>nhIW;DtLU)zJtLe?t<;x7KJ6zGsD=RGb z^V$27_nT?^=rQuLSk2Lv@R10Cn0hHp=j^aLR_PdNktZELil&PIW_YLEH z?zlL;g1M9l>rzQDbAH@1baEdG>_>X{gR4r`$G^hs|qL z6eIcCCfFalNY8(jIERnNK6vopGs*L`2IJC(vN2YR7Xsz=Vd$*~jMDbY_Mjl)DD0$C zd0rfrs^v?iRFn24?{zP%?)iSh^Oz8jDV*N4-`_NOT$IK8+yys5PsYv+W_=WT6D}OV zDUo>o1W2m`^6)la`&DFoLIMvL7cH!gD7@DZAVjT~tKqFuNf{X$Ec4^#XK!0CuGpE~ zxw2f%)cH16^6rl_q9q4F+ZH2r^e$)UB*vv9oWueVL8Mw0Qzan0Bz7m$<^0`@t;lrE{s`sV9O-_zOW zhXV=O-M@bo;~_8c$w~w*jEuy9{E|B$K{#F<=t)nU&5AJI++j7fEl#w=8hE^ z2nyV*qzF(&aoikDTi)mL;3HC#y?=E<@3g2O^Yvh*oqc=jZ>uWP8TAm9Qm@%rZs4PG zt+t$9*`0EGbwmDaYC_HJ?R99s2Vi%*iw?O6P_cJ36!2X zXi-}+ja#s#^LakK_WC(Sb9*`7im$&ZT)&2?-82h_U$h?%)Sh2l&_|X(2_U&@>2?j7 zw&snCs3fsk40=@2gwMrgpHil6`&VG}*VDaTWU$?DA>Q8lxWcDjcFvmgJ&!}=Xox%j}^ub@yrI#mP zKs!`5O8J=q|IzgB8G+Hb#kjfZA(&=IN99D?w|2zEE&y(&3BC}_ZAX84FLJjM>kOC8 zm?;3{QF+~F5gld0wp`f!eWAzT?!y}cMY>lG`OSY$VRFeU=bbHCeB6Ru#q$(*E8dhe zgEh`0mul7XdpA6LJbnIwvzcS137Z9?mz}uo?d8*!>3a2u{omZ&oNmBpPJ@Mn^(p=c zFI?~S8efWx-jxj*bBIzU3D$!L0b;e<)5phE+pqAHy8~F3%D#hPYy_Q(Le#U1i>Z%s zaUrm}uLHTjihy}D?zA_`cK6m>{82z`{_N*l-0c`C>Ty6yTDA}8JdTRG4_rBcpto^h?-KiQbEnIceeg>b9NFL9pcKzi` zFbig9YwPL?aqfHq#}%v`Kzg^PEn#(_>P9 zVWH0WXguAt4-**!%h?#|0f&gbOLK(Pl%`Wzk>0)~1k(F$S5EtRjAFs$c=O7gE&aE{ zh>dpVeQi7LmFDF$HpD@x)RZk$l?i<({*{}LuRy+FQjpslXc*Pj)Z{uhgKj|g-5#fn zjScr%`9p6_?bv0#s{pu(w*lY65RP=8vv9+3LW>U^misFl8RPh<$i{OzH_0Fy+dCXr^RRD&tRy=(_ zRI^|9_PWer}%gEy|dt*(LpPgPeQ59Rm$AEJzJQeEal!UjFd%@;vw6bI&>N zv)yysp5$MEsJ@%G;gxd_JB^Yb>b-Fzrq3+*D(gwSJEq18PknL?u>cc z)fX#^cUH|$9-hB{?R$jnNct|ulZqb?@<-4s=jWi?hraSQimLx=^%z0XTNJ+hlpUy8^eLI=XcGhfz#V=z1Lj?-ezI84H%V3R#D_|_q8%63D z;-Qx1$7KB)gk*7L;PN}k{BcNe+u z*p8vIwfm0p9cqg@c-v#G`nu~NJcF6zq`jCNbb12vH6v738RnxXq;jsvyJuNyk0F&KTT zU zqNg77+jn?6)~)lMp*A^)knj4&8Q!recRiO z@=TpAV%1icn#dn7jKNhEr%J4+7)!5S1pz}!J@~siw@K%hJw`$I*UP|VMHH-|G|jubey5+o&?O59)cbl0bfSf-0dPpP0~A(nAl%g1 ziC&|Hnm2obpF_fJ9BxuC!9%5F?`y45*ZcNrsh!7V$}{vcA3MqAxV@0-Bocp4ou)e~ z+ub9G-koig4^!k}0TsISp^O$*Pw1lG2!S+|>58S8b|+;f#mMVRDm9#6+fZO{MG)~l z7q9$&KObJuIRx_-4#6Dxx|ipE5B6HQ1QfkrS7(hSL-%#7l`g^|*o3U_}6Ok3p`fADxYq+}=)4oR1C z(nxWeCYq=6{ideQ7(Lna8SWge42c^iHL_;z9H&s-Hak5X8+?n8 z_2hnnf{6JIM&hkw>H5|`mJZ!M(nV07Tsl}^JC5!JAox=j$n52%mWaNc@#VRCYENy_ffyG;Wc!{1H|oW`iw-(W0?^KiOf zxG?hR#ra=LO@V5$(}QJjF-97+u-U}Nvj#!zlxuVt0}bR|6^~n;*dTMrlv-!iko;n(6mUbyn5uW{4t=U%ub?6W%ASPFbr$L%uw zglim6F1F5(cg_#V1X=*M15#Kr z0(A;Z9AR$%wv;V3r-KK(4iehWOSoGlyK0~CmRr7KNg-D|&bE~I6D5@>Q>BkSj4FcC zg0hnxJ{)Z7YYngO7cKS*Nx@y_0~me|$D^Wf{+lAoP5HqHnLq3%)2X`r+IRcWL{6 z`a%AeAAZQn&nj;b(s?b(?lO_EtGeh|xtB!2M*W|gt6mX%G}hKv3svwGL^c&K6mk1b zYHnIA`|a)K={c?6K>*R&}H?&+h%6E@q&=SyGlrSWAY zboO{_j(*!72U%`iWv+(09rvMjJ^(ISQQb`cJ({|bQ?c~UbK~4Xx~iD~JGL61M9QoL zt@@e3316l^Kn*2QNf*0D?QK`-zOuJgj=J5w7b2~IH3Zg z%&f5uHRti8-Y%8Y4&TF5$kQ8%pLlK+ol!LYbchz65yBUj``zC+Wd96HmWg6+qV_2h za=Y*9VO7=pbB2m-aF~5-o^j`Y*ed8knvd40k?vIrc9N42e?c1-aT)8p& zgcYHOz-@)lqBQ}xCS5lPhaiUf*-H_e2x)c^SL3FTWT`7-?UU-d`>C;%_|Hj7+BrFW zQu_vqa-=)k#oKyB)6J$VFdxK^iXg?2(^|Q%_fH28-_%Y&!fxJEaI2r|TC{-#U(~UW zK`zZ1!BY8kt1L+Wo+W?$#|s^VZRne_eEtFsaHte`L_DT zUL_Cw4U@R2)tC$bujPPB%2Vh;WHU-dM-_g{q{0$>KrfolMW*c>Xs&ygX&($HUWyiL zAzjZDL}J4vS}cZY7b|Cj3-m4v=|Ji|s}Vda^Ac-Qg(MTTR7D#u5&}(z2$#NCqju6G zb=w=}nMQh>KKs)N+bUW-H}Nm$VVu4f%dC!lf8lWooLtFK$qdjDDK|_aS3yxBbuL6= zJArd%FHpi~m;PrReK*V}YL#2v6b#~!$Xfa7TEUD) zw^QKjYU@1sP)T;{VxZ&ttEi}0T3H>iS&Im3Un+U1ps08vjs+o2FDlf~TxCq#+~1^7 z_gHU6WKJ~Vu`;E(v>p?fDvuZfTR?gQ4rBXznKC;X_hRIr5;}1ImcA6xljpS8 z2{e*%L8tqH#!5lcu;K4=5{tRW18MZ;!>*fjrihIgoB4k)!6(aSomdt|2YQzq=#L5k8`a#y#WgJz>}9nXW##q;*iDa80otO(41+} zDX;Bo2%IT5jjLuWUVoo5+I4NFy4Qv~trm|(koP}`;lvjB?UkZqlWSmLA7R^Yq5JY1 z8&fMQtMWcY!3}ykVK&je=`5Z-w4tL3FrlqlU#CvL<~fa2$37orEo;*w8pK^J`C#Wg z{&wR`McF~iz41Glbl7bhW3qcN*m&O87%#RL^XaL)AnzTWohFi7xXsw$|F|no2BSnuYPc6%eZv%X@+||Z z!#uH%GJ0L<5mlg<5u|O%$Mq;FG@<}iZ884497-t1!`X!fX%uR`heA@_&hGVaGYcpx zJ!S7Wz8$73X3(OQ9gEoQ21XddixwaVkFTD#rt$xn!igzldKH_fPs28G0Fx-dJ2ACSc(^L9jfCyCel8Qi|WDP)+ho{gQc*tfnVvCf@S>Q&YIK_WNKa0Y4rDW`O76D_uD?TTA@_Ksa#E3yFjS?C$MsH^HM`y2VH~*@h>DkvYkYI|*_+|ntm7<52)7tJ3!=mW z@3gz^^J-GPjz!&@W<)Be$?2{kp=@Y1c|G3BJ3*bCYe5EeJC*RSOUtodN!} zbg1dXv#Lk1aS93`cPt1;5Zp%ECJyV6N;XWwfy@lDZg$W!Dy`Mk6pq9;w(_1-74fU< zxN~hTJ*bajN=Acq;NQb7dpjIMVNeEa)j#jYHSUdRkTt>}vh%^b4gg&o?2Uz6?$rCO zK7f)NiI+nuUGD9!LPV=8fin9AF}M3JD1X z{lFUBP>9vjST9GVXloD+?*}N0kKOI1J#dAZ_+z{)ULhQSyrt=WK z0UtTXc&rscP5S&(}Jfh7cdBwL#%rMyD=2 z8O(5T3jEk=xfp8>>czgK>3%IVk~v;>eY{c zcBH$FPI6ht0lvdEqImCvTt&7ym*&w7r;Ep?ilXH&#hJ+SJIjD~#u51kl@W|XPdQ+L zK{`ECv%Zvy_@sAxwNDn&plcqS%x_&;semKbp4yKD99c^>1!Xy5LeS}k338vSs)9#( z5JaR7M1d~9-Z~tV9$lrrnmm|n#-J6@#8{;taFyo4VtZqohQ;*&l`o1Tqyj4oi~Xo# z<`pOdFHxDA7?lOQzHW3TRL>h@QJ9j^LG*uHjw)m3 z(Dmrh&$+C_#ZT1ZbM@AWriX}Ba~U-sct=yohp1sxNX2fd(28?iVoP`MDIIMIVrA7P`dJ0d{r}i~=a@YuxU<}k6Kd-3p0EvXB*w{Nm z7}U3Dfrx22>{q|k@78m`h!}~No_!skJ|&LI!A5rO5Gw}JDTc2}h}~78W~6%spLn^~ zuTH^+t6$olGp8#eRL|>=nF9h71m!Ze2?jTShe$b?kuwg*xL613Q0&8O0XuB{`t|7Y z41%B{@5CtK6Ka$hXg@j`zMZIrMGdS_J*cSrjc_0{+Dv)+Ljj?Bdzi4D6q|C*)6S2; z%Wxt_4rLm6MjZY8tP%|B(62?>*m;A(d-hS@hE>+f{^Qky919NiJb$Z5qnr#>OFDzv z(ZI!>k%)m{g#@rVlEy#6>!3^}%{Vfp;uh@cJ{&F)w+4|FhYucyYF>Um7~lxz;$N#{)*CV^5K{Ac|8U3&z;2v$cXvP0{#pY_RM2qt87h_rLH|!T z)k!p?1UAgom3N?V(=W4G=%D1d_=mb78YR{WuwuJg7d$ZpG^!`? zA$4H7ftf0)pBW9g7u2kZ=P9u^93>=rU=a=1&@WEKy-xrX;^R%p2k+Tl6Krq5VdieHw?KOB_VZoLQk&JK6Z$<+j@ zo5zZG7s+g(WO!Fet9eqOVaF`Bx(l=zxajdHHCg}I?4-TXjYIbvoIr&)pvxFxQv(JV z2PFi6Gx6?K*9JlSX6vY|?ba`>+}ko?SnfP)oOIGVsnn(<96RhBt;akBxUqoq#8{ih z<*umjRCg<=4u&rXtTl&O8|Aoqdfq|Cxga_O34*Z~$}Id66!>dEK+nHU!*gp~HbFTL zVrJ+F{W)}R2jUCMeYJVbWhyg4Cj;W zWj1&%TW#Lg2Ex&gc-Ns;oeObG%W3j?Ivv7#sPX}Uux5Nd;3?YuHW0%69yQMXvBwz)&Mqug+Oo|(5LX= zsnc#s_1-ou0qm7h{1N)uZKwc-{3l&@Z&|Vr!Tn9skf8r?UrSz<8vl%6ROGZL)wJv6KSr24%#_W=BS@*qCf0M{;XF+VTgF zv>pQVNTY>VXi|;H&E%*RnuhxzZe#bTIw0%NlbC?!~BB~?V{)7B@E#Ud+kvdi+L~Dd! zml-e6YjdXSU>l>|y927FM_q?az?fZjVY3)^?1K%X*ZyV71KVbGN40Nw4jcLO6u}{R z>mCS6mbo{#1P!yBkKB1L9nu5gYE;2O&}cWokp-y|-6GO)PAJzA{wMnrfVIi0vI_ldO(M^OuBGmAygT?XUv}T|H7-Rq!e#=^wqFJ z&jI^g7}Njkk&v4A?{7m82sLctjbN1t2FH?;k|O!o5H?du6{^z3RJD4Ybqip8LuQ<% zgG3WsLk=^kNI|EHa7fZX#c2v8V#wV$#UWeviCbA269*K?f_i}Bf+SvuYk!@+x(|y* zMNqaDV5fCd9@UFMe2HGc^S+FlSB|Cartj?dLebvQ*v1cpaBeQlEy8oSiU!2_OZ_AranE)?u5jxoht)dYLi#T<0i`lO}pw`Q|h`j z?0o*(UfsVfNV{9^o}T9~Uv5KH5VyGR@4zpynM;IE#`{vpJ53=KwN-N%co!ZeN_W)J zyCA~RBct1cg`9{gRNB@(;pdL{ut!o(NkV--L|YJy=wHCzys%IN>0A`!x0;ttM|rNZ z@M96$7q7e9@ONN2-X+71G`C&A2TT*{VSIYa*6tU3Zc>AoqxH-EVLWKKnbimZ79$sB zDlNF{-qB$bC4jm4YXiHL^GV;KrxrK_O!3_Pk9gjHclJaaY)lWRV~su zBt2ESy&&YmGat$BN5yD+w27lE2an@Dz$?S-kDyd%UR*wsJBZ4|L#GpBqn$VTqBefM zxOQu&$4isLxogM9H*zFtr9mRh8FiChqk!J82xk+}we+8Nx#CdrE(;ctrmT@~N}i~d zt7%fn|GjYNeJ?N_JaB`K>TG0+@7P|HQvT%5$6G$UC$}+KI%HIi6h!^WHIof#2dJ4r z0Z1zZwGY%CVCD3tFZL5BSYSGnb^j$?RG?O zI{?=Qjn2H8OM(M@7$FEy1;hCwh)fs&`*0*c!!wjgF0uk6A(dRtkD(v?eS;gGgh#e- zo0_B_=+95mAyV*on@W#!Y?Co3;O%1gsn! z>dW=(OvxrNbltj?FA6A}g_vp3|AR9C3pDNypbiJaS_yj*WG+OmMme9C+#{h~%+*-v z+hfLn5DX9<;=cY>50*Eq?DYGH{07i zKrur@=EkDQiohbsPyuPLU%$?2Hsm=`4*W%TX7XV=*dt(79G?Gh=2v#$uRU!;U|+vC zgct}#ThyKwsWHw3=MoB;!3bc@J@YxMu~%AH8`5$pbfK1gHJIe2)WfrH4}e85Q4eI% zl01?dA^U0aQcp_|`j6Cve;b_5$3 zj>0FPMqaH}(pPyVptdb@(-MB&267p@5BA}9ns>aSG>%P$*(`~qo;6Kh4{g$X~M_h zV1%cqr-dgJ_|KB+Y}`@W6&_TZ87rR9-lG48_tn?uU%tM|K+)a3-S)$6u+RwkqgBB= zKQ-5@*o^**<#LDdzAzd4$#Ev7wxg%-&hY6Y-#zvdoMPNP4#uAL&A;%+W8w#x`kg3l z!_Z&@6W^`EGnLb!v*YpyhJnGst(A@s4E=(GgKNeLc#TbWPM5PfZhWCy9mnsBm22Cp zRZ-K^e{sq&QJa;}iPtx=cC+p?PafNB65bOhU3^LZMYFoZ)3T2X=p$L`8iM1zIi?%x zSBfn$Jb(TiMfu;)rg2978V294d{qUJiZ?yh47Erl+S* z`2M&<`+?yHV3vi3L&E2esyKfXOyKC$ldOZPV*+0zyCnR4_OiaAaSHIH^CrOhpYdJ1 zx%c0}ngSC*uRU2S2A;(jBffO`Ie*os(Q9R{M#SujxH|$H`e*YOD`@cdj!k7z7Mk6?J?@K*7Rk)6 z0OlwxwBwl)5D);^2e*U0zS6h3xqj90PX6xYgQKrRyOd)uL&J(q>c<`%(5^~)dJneF z)U#63m?GmnM~1yJU`|cBUQ1NQ8H_Fm$Hb0Gt1ZA5?CTG%`K%w+i!5mvy*!kco3-&- z!WmzlqLVoepwtEyu(!~wRwrd!%6Gr{{5{8}jeGrG39wM->+#zAFL#eCX#-iE0^Rk$ zcftTe0-C|=?kuVsz^(#gxKm(QhfXQ2{?GjXHGrE==1X(7yqxFzCT#WBE?_5bGqi?{ zyQN>t&(1kmu61WA-SC+E=<~9T&T4=qhF^ZRN%Oy^(^uLt3|LC*Ee zLL-EhymgbB014371gnF|{!V=GI`2uiKj*7p!pEgttM_w{24CHlUEh#qj{&jlke7&* zFu~TP=?D;almY{n37eo!Y`9#V5<>L!NXAM`n!S^d_Rn44U1`uEvFT!o@9 z#~wZv!>k|nt?Z+@ZL=p%oFjIs5y}!&5LNmnQ$EdD7CELVW(tfG9De;W9oFTY@?h?p z9~F@bxf&dbW{0&hbFV&qXYVDJ^8}fP!x}DG-dL1~yu$Ret1Z99j3@RLne!Mc6wmqz zkz$wQzKWinT$b+RoQtrwzv0H=h0kH%X|oMXl6+hHRkiS0z8>Fl4inm>sFF)bL|FKj zXr@a~6P2{*2^&07&^R>{XLZx%ew`;FZ(GvPb=NgM7h5zq@qoM8#c=ocYu#@Di@O0Z zqc3q^VXRMOIjNss##;V>%D3^U7a)r(xu~CgYk$HoV9>GNO9Zqig=9U8ZH(hIA zIH$@ee$>2v@DH3-5+3xTHnJ3{k^cd1QUz7XeceQsUG$NCUx#XI#{-JLzx~X0xBS2f zf3W+IcRPU0QqZ42^8SaroLvk*ORv@e-atp+fK)+_-=n<#z4P^@hnnAR#XkI$ok`~d ziM0=BKDABL$fK5dTqh|mE>-pB2KjcCG*W=Cq_WCiNKn(Rha@k$>~bvF015Q;PUKV+ zLYsu&i-fgm7z}eMGHmj4hjJWRrSf(yV!VTC1~nokx`Gk(1sm=wWBSLlma^%$kk=>t z-s{XQ_r3ALE=&$R0#$<+P6j?}fBg6A435o?J-=ZYM@l=iOK)lO{{-$6eyow%qo32) zWMb2vibuBEkSA~vcj1~8*o}ut!lh|X%ljL*t|T|yF!%liy+Ur9=+bH%2&0KyF(boo ztq1sNIKVMs@{Uq|O>%~}O#Od2uUO{MFf7A;u0BQjBB(x|5t+rH=% z)M1fd3Zr_r_V5On-0_amL= z=B|kGtVB-03Y~0wp3M+tKk2bPgT2f&OtgTb9^IT$s$->Y#p5Hy(FGeD4kd8c?N zIwrs$64n+|qf-WI0gim~pO&2~y2*V}93SqIQLBT6lxH_hryv!!sJTv2i=s+%U$-c& zJb7?S%cN&uuX5G&8Ub?oE;V_{QRjMwjue#oR=HUD;@;8M$L7!)4);!{S8^dn16QbZ zzY-v{kxrtwz=6T8H?&^BKX&oe`b6<|z@_6NQdHVCV>uR`!HKo}QdaHCwUJ#K{IxEu z!*uKG2lc0^zD=Ed&|zallD?bFH{6>FZf+n@0PbO+&0gEh7O!hfJ(*8!Zb`q50dH5w zzlv>TQbq~BNiwU_xVP-GR65S77`}kqxJ+lw2fss@4Scvn_o2*;`KM3|$`jcvb2E~} z@bz*ZXZH9?bada6>*(0n2k=A#S7_*Zr;CA!&6A%(Hgq@C6+Gi=kBY2%M*>rWc#-nr;UAzlTCU-*a%Cnjf@SpCQM%=+wp zvCHBEZ{qRs13RXfYnNtuv>63@Cvo%rYO>iHxsj12e%9b{5$a4w!9Y=!upYjS;F(}Z zhVU1ya=V#OLAMgwqHN5 z@1R4O*Sy8Q+s1!lrJ3v0xuJbrtV6p?r(AAw^>gIOfWf}A-*J|>lN$PlZOE&$c+rik*-u@b$ z&$_7EO^_2i#B~2fq?=`3ZjKyUO5tjby|a2ftAnZQ&ODS@qOqW{v=6F>cC>xW$;Ys}~J4Hi1 zA=K&lPCF68IM?TsG%uIvpWFuzoBLP=KDI|RaCpdIPmcCHtWW+Tom9$4Tc-*pc_n}* zF@N3-P392;NG)FbwX?fndIH&?{zG|x)H zoizG=KIIL5&OR&ioLy;_L@Cfj{qhk>T09AB9TolXS^V4OebuEl@w&g!#+cNh^(Q2G zCIT-e6~iRZR|$y{GWy8UZ5>Wm$C&CFzBftNZ63ap#Jpy!Sre>#&lX{%&;077~B!Xc@yoCES`sAUKICv5vI|WR` zW}-9I{U~?rmhXo48HW|LOCir~ghc%1g)=|ZNZ7Sy&l#TD!V*i0YQsQ*Ha4@)Il@3p zQi}`_5{mtyaN}!2N70A@c{f9sHog0Hm%=^+8cLe&Q?!Pj9Dc#X3 ziF_FP&Sftw!K>2zASHZA3yn*4p^a;cJdvKjbTc^^IJlPd`nZ9txvp1t))V{2+C~p* z+>My1vo$K2nsva0a5PVx(p#$+H$KiLn3h+z%z)S;Q%g3ip1NUg1R@V$n1=Xv3o;1EXVb^eSEz4#9vu@`Uv10bn1wx!vmUi;*rcs z`L7@pUTury8n2#`9Z5R%456!!OSVE!7@u}M@+B3zsCplJgcp0^=5Tx7GP9J8Y{_`4 z(@ZpEsSRq)D$t&U_NbvW{wlt?#Y8ZdO$|#4KQZbavJ;bJ#6-ZO#rG(DnIK(ZxN6c7 zFWy1zIHSJi2Hcx#zFZbkbFv?{W@YS9e+0_DdG?P}a?VNPB18?rkD6n0K#LbT$)x5S z>>UDwB-OGfFLkX{*Bf1h;?JaVWK(?)#PhfhQ}Y#c>xhv)f|6@1L-G=g3qt611F)Nn zHnPfV3m5ErEWqxI;%`3y%anI-2Fx#dp{C)`buu7^_+Dm-OkP^lOZdPlYUnWsaLaN?EH(3>_sO98?~! z$hUeRo#1S+bK7=dUGS2@vaF_H`YJ>sHcs2n>#V76rvn1f;?}FNH;I#@9BLvK0>in9 zEGX@nE$oo{8u8Jo92LR+*vl>DMW!56U}tx?M&97VPc7CycdFkCan|1Qy2Af^@22Dt zGV8B+j$=LiSQ^f%WOs?J)=2eM_8;}9IY~XqisX;YX^y(Qf0f>mJtH{(9vBu1d|&!1 zo5eIdU4x&1^uC)mZl#&m^$(>*(NMGnv+?NEcsHa0FrDSvSarJQ7-LQ_qn zcBEsGH2(K1>8%frL6ILTiZD)*PXmaE?c$Jt0`UEeRymH*s4`{wtjcz zz&$n0(K@1SQJ(nIp2d z>oXI;NkL0LjX~=-!|E^7g5$$aEa;aHeEuHPO4w{)-5XxoJ6c^MsxR$+S5w<4)%Y~I zDO|^X<0|a&R_B#CSYRW2!)c?;Nkx>9MUgCumCu5&s(Y*#<>rBmnfq+c{5HH`MS;E= zfyhj->%PmBr}b#tQ`!J?Pe}xfZF$@AwViD+5#bzGLt)L!n7tsDIcgWl_L3>^>kqbA zwWNx1z9FOPr-*tL9_o(8B{$$Mdzs^F zX5m0u@p9(lcYNk+9+ABk=3P&UTM&uGrp@K&0E4H&`xsA4z@qC-zAz-;OxV^4_6;% zb&!{@06Va{It8IMc^IoyF)d89@E7htCQ}F7P)s=sPoMp2I_3mNT8ghuwl`1vRF5qb1#)?3Q(t4ktUOu`r zTViN|?)&KH>`Jb(-7=nod!akCiU3x10euA5kQvnO!v z?o}WVK>}x+9Im4ntGTQOtOe+AAq4;R4{}V}AFcRuBA9toTmH_$p?MEp@6muj_p+h> z;IS036xHInmhN3KtLR%t{mdPGn|&kHVooDnLrhHMo@4=Bl?^&0+MNy{VtU*{i*m!O z%J2mb8}_*jNXd!*yB1M++-Pjn)h<|f-M;uQqG!Y7Zirb%I^rx^Gl6Eit{U^!ZV_+UUqAXbZ zC?bKpqUbF$6r{JfQ#Bjl&@I&cMJRB3$bkfbkrBd?YqW85Z06*{)F1v{C~8km_L<1( zU;8KjwZaaW4_efZnx z1nx3SkjVz6{x&_xvUki(*S_%f6`9Je;sXBxL0J*(#5VZsRU--t;1V9@-w{79pr-T@ zV^TMOym~w$dCwUlE#lk>5yOg3?=Y110>&**0I#ns!K<10{>oYqofv_J4J{+-x$*gZ zsZLFGdAZbBfL+CBS5XD5P-c(a{!WG|+GL2WIl&XzIOLW202D;qECUPmvvu$Mj#LP= zTd-&1VB4ukIgMO)MSOic*QXc83Ei8h$fPW~gBqyiNQ zA~kx!PytvyvI%^hS}jzi#LWR2^q4XEL#^e@BiDb;wpFoH6}x%3QrN#-7E(puzeB#W z#DbPcrV(GgC1tSbnbp>;DFt<{DSlp!V~eoubl4q#m=9u=qTUG{)@P%=wzgPUT=1)V zzar|#I}Ma5x4pZ{={h_hP2~EAnF-VN6O$W@{vz)le0WwY>d_O-scPnEz4UvWGl8p1 zV}4*`H`wQULKSj)VQtDG5A2Z$&SkXJfXgJ)F)fzLiEh1_jXoOF0iTz$$Ku3&!T~9JXDPUBZfpdHqC@rYmUC z%$<{fs|V=g!Q$`(^o}9CYWcLI)gED6Vk% z)iY%6vA&kH`vc4ssK|&?p;qn;liR@Ggmo}PLDeYv55$VnPph2fP9G!_{52sp31LL1 zXryrgrJUmf4_aI180+?FYrh9A9Q>}fP0XKP^n|6c@32Fb)zxurESA*LqQr*|EFG)l z_wWv+3e%TQe55*irFUNVPhCkz5g^JcDy)?qs^wc#(lQ49x|Y)^hx}KGOIPB+MZ!rwtr6 zNT61Z%GA+EQ^5)gFU@KNPIWq$Khg;rIvgtf3gHLq+!(K=cd`**ikT7fi@owP?q1nJ zRAFjjbjAH^$&0QFE7PAzjX|(*P0@A-@GofdR!uLp%+Hp2B(z+9M0`j$Yjo^VXm*V* zlD+=v$?4MDcy#mJ+*$)e9{&Rp=5XHxz@cW^6c6OROUG!BRe5 z>I#;tDcgi?jLw^?;cj>mH~_7O#;I|e<^Ke0+&?o(0j7ic?%Vc z=>{(|P3%uBzD=F)7SLuTtVLN&LYWbfLJ*>0kJR?M#)JaIx+K_?Oq_ZHEn5cjWfQON zjx-!8rd#bVKXkY2=DRPdprMW3Qdr={ZWVi9=cAUKqb1dh@*<7*j_9qTi+XfS>IdIi z*=^K>#!$T!Ifw9P6V%I9fhddqC*G?#>4|%R!Y%pCQG=7y%x5(UeCE;-6wOVXijk`^ z;;WHTY(2VC*B4iNN8Ouyy2G0uy||}kx0QMGxVzCIwc5#L>>uY`v=_vnO3`3y%nUXftkG;;Of2;yTlNbgGyX%xEpG76Apu5ljs9$$?re)Wt4b#Y|sjK$K2!)(URv+*EHX$ly3Xj2Q zirsf)`@FAT-DVj?i-3F$%7+oNE&w3#c3Y~pmlrDd)*}+GEcAFw@Pv~+iITEK1_9vv ze)kk(wshV27?FLC|MhlXC^rq_Ac|xT(r!iNlUs3bDT6w!>BKuhbcd13OQ1kMg*q9w zO;-K|)<7@r?j4k4b?tCFO#z=V3@{;a?Mbgy+tdN~Os(9$`1gV$gK1g?>V>Kzh6(6K zQ-Ip=*ODk8kxtI@S*`c~55h`F)2i;m z$ezWg)MkkMfIql7w_iZlOk2>5ELYfVML5`@yccKQd6~1(W}_OPC9OR7noCot9Oiz> zz@muO5J>Oy*#23Cp$zz}aU!ufJ_yUO`U z+EYDBR4rs?F>5fqid;?nTd~Fd;0K|A`fv%T)NW%8wCahNufG&!2bJ8#`8hwT4_f!U zF3aBS5I%nOcXdBXhp)N9$}6>IBiEHYGS$OvVwlaQVM8W#6|76n+77YWSWk3gpqo&lyVK_%;f2(%D2Mc4O)JOU(hd|_9x zNHUE3NdqL-DPA42>@QT&`HR|mZA5|@g_ZM@jAypW=jsZLY|T!4p0h>GmTK_wuie>C zIb4q}PNn0Q7OT!VrMSvd^!Pq1ibR1umN`;q$4WgIDcKeDl4Mw>38*X9u`e=C_I^kM zT8Ts`gC+nP$%eZ;ZmJT52=-^EZW@_UtL~RrrLjTNF}qU9*klQP<>awKniYR-V#wCm zPi70Q3BL%(4NN+Q0WSUSrn1chG?kehAdEk7Dz4v3m_V1WKAjkWjNlyC8amNfnnk|M?N{^E`DIows& z9RdA^Y+mt-$>DYH{2g^9YYt4gqt)Fedfmw_EG;fM7*fEL>6fo@iMfiaomKpg!~{H+ z+Yqs+zCLQflZjea>!+TlL9iR!!6du`(Gi;g$CyS1O?OIL{{DT3$=Okw`ZRDF|Lkak z=1gWa5B{_L9kwbaKZtL)0t3t$E}QZ`$}F`)R3*eyp}IDb^v#kY zRf@1;A5<K>5<2jnPL5&?FGi&b5gNdDy8NuX49OW% zTsG3Yo`z%LD`!1vbK3N>xl|&g!=25(bLo1(=U4epWr-!_)kgz#nnry8iznV$TT?x} zoWiN-0FE=^oseqV)JJqGs6|`w?7TQxWTZGqX60v;c0H?^w#7V^-kNf0#s(tRXz0pE zsApheyp#03mpg2)qS2+}H#O+9Y^FG)#@I8pKO+{*DwA75ePhkM`c^8C?VvD(i>=6)cIPm_MrA z1W)2K({vA$QCs@eQFV%mMniVuO}E)8m@F6+re3jQqCK(vE~-`&(xF_Z;Mp1I=eNQ> ztj;v!qn!Lehxg>WoAW__SMkA=Q|OlD>eFQUfNL10K2a{GbIR!?VBSOKVWr=NEQ6|q zDu#dhQ?^~o#lil>emqSwvFW{6KKQ;^umn9K$Us zS1D`7=vl#r5*W=BbllV!$McjQ0}X!*E`Ch5;8A4gCzfp(#ObHvC7Uz9Y6H31mjx4q z8{$IN0>tW6tK7buH_ODzfzI9Q6iRSlDgqfbTMKWUX^-2G-{a837LA_bhN2Mxl3*H0 ztV-J8rBn?KX5N@^37H=6KOdjb7D%A3s%IN_nVEG-J({iAGHHC<6P4M^hNwP5=p->J zB}f2?kZCSF)UJ8WC&n=|e!Hl=S|Q)S`GH;O_^PPegWb=7tf6y4{omMt-UBtXOLW>I zB>kQd0~ouOPc4m|l^k-QuEWbOwn*e1kRSw(#;s8*p}BXt^zi$A>uf#du8_yX(Ocsu zx7{G|iQ*#x_P;|Wr#Eq?iG*V{)j_eNBpZT=uTFOeRk@C4xP)DW<)k8V zn~hi~F3sZPMc4c=^m>%;U##T!kY?Wq@I3~XQ16WlpFg{kVv$dDta;)(AO%$DBD@U) zMu_}brNK9r*9rPMCieDGgM;44_7a=DL>r>bFVVzOp3QgvlB*nA!`cV))%Abam8$^J z#}~%GbB1|#NArv`#!0`bYdemc0Fi_#n8KQmt&vgz^wAckh=*5>3U(sa+j&*!a+YbX zh*AwfKj(~2%OHZ>L}dmXf=x4nXSQaj;FDa$GXKvCT+!KrS<=_1-^a38CC+%rhxf)m z(sVVD?7FkF^r-Yzgi%fq>0rQe`L~-Z?98C$8lYCsd-D56@7ok;WexKi~T{@nY=rN5Ep6-LDdrGO@5W(KufZdX!*H^Fa> z;Boh_Ujd?%gB_K2EA-Hg_=`_3;G$#fH>OHX5WF~t(S4rP$t%Nf2Sd7Hm(YaIw@PU( zUwGC9uqiS5fU61qppO02)}?>6Q7|%wG`+7mRFl zkw$9se;EhMt|#PaRhCq~`r1gG4t#D z>=!xX)7{H!>a6Fe6D%$*a;o=RU5v6Vb zTPz?5K6%nIFmUU{=XuGSV}-Z36aq%QF(j0mb4FVBJd0#*R5iEX z^w%p5eWUE;UxPaQZIN`{@=bDi&ytV+6bFgx@Ka4e0!*-c-7w+n(h)-2ncpi`_b3@3 z?Z-0Y4(E)@qz$h{$4paPLM=>tu<4>$J;(}HO#EAv!rbe^2o)3c_3dbEI?4PwJT)Sp zhM12>%^y+~~U}x``bL2It%%oI4BbDXeyH4U&Pxb zp!w)_hvv;BjYo0u2`PlA(?$H3Z#+XfzeHKrHZ0jzU8HpErSvM5;Kq?K!N_O%TFowC zfK+t=1A>U}TGsAH%53J&2qA!4rEZdw+J{Ql{0@Xf#@lvYI@b%pPDEa)`MJo=SJFY7$<-!1weIZVJj>*y_4(0U|_;NmUd zrk7yU*>Y~fa`b?$=DxB1qo{0MsMNRObhIT+tCw$<@Z%FCV#6@Xu26ijK!0G$gMVqI zMY*~Qr&6ZxRumQ(kRM)pv~-^xAk%NV?zvL3uqHDr-RX*!7b;kZUXZ}qF#@`+H|;G9 z<}l@AgG&5;>SxpdVuNueex8-Csx|Yi7gbHL?F2$lwUW^+jl3KCmo!VQ42JJrfA%W- zvyZHq?V5}Scxy;Xv-2zC0sd@1Rj67}iRb#kI)RS;K>8;0g4~8|M2GlxN5FYLCz}V# z`^j6Ve4#mAcz;mU5d`(rP?56hU3R_R%#gQ7{N;Y|Xz+c6YJG=leVdIirmv6Y5R~`2 zO8SmnVc#!_?zIP2ndnpHluw^x+%#RJ?FYV^dA<*wu*gyjU6IVv724{CgUP_djY8zZ zE*sDw&&8G{M(Owy}g?xBmNR32oU*%-X?ML1zSOw2|^a)b);graW)W&98;0 z<5&7u_CjLfBO+XW$i3t$|JiA17W&B7nN$w<3i7l~WIx-8Qae7m*?TUh1zM#;=sdIb zAoQ7$WQfW^ubS^4Gw(05TcyU?GgSa?)N4qJ$)e<=UF4+SikqA=aZ4duYwM0onp?Xf zAxp55@Y2#J2EzcC^ZoTB`FG;d@;5%x;f!UM36=7qpC9m)YD9bo3jxUW{O*Iwg=cnf zgSgRAUEuJ#uc$;FJiZ;R#xa({@zF+XPe@hBx^U2{xVBj3#f!rJ`{fdZ-jqw56ZhEZ z=GdsWUc|NFOZ9A>$G@|~ooOc>s6}>1dnG(~TPYOcZpSq55@~09-k0md8{Q2u+k|q5 zguP&sn}X%@U2frhjD$$hmcGA@$%2rF!&QXsvi8{OHyVicz(D(XN%ByRbkT7O@#U>o0yt1t5@3Srj{RY=5g1x7(GxQThu>OfqIUw%}2f9JzbTr*m!@3UVaEwXgmPkmw%^iq1L6r z5UcJy6N0V3bQ_)C39w*Qi1L|&U8y6{(|+jE_=h9(ey4N$CRbdYW+pSmtnLdtCt6A1 zH~G*EbCDQ3uM$5ID-;MV|7=+isl-%uRj&JW7gwZNxb;h$(6+`d;VK*x_Re&A`3(nq z?}#&p71@7Me-J#`NWMpgeaq6jkJw9mR;FHK89iJKzXR!CH~w%)lH`2==&=EOQXrn2 zkLxD=mlCPP-}_x>N4|4~$Be$8j8FTjx7#W9#Vok%0vZY3Ly}t&Ab2pSyiO;*)=2a+@OLZ}){Xmt4OD&^jq+ej=D`Q~ z`(NZ1#$N=CX{5Sp%};Gz0TcfD>(yu7_CX%#@crx0F5@vfH5ucD{yv3KEi+a8igbxd z(!1rfBRFe#&--4|4*V6rSjRzQ{gE_a#a+Vl%Oc0CF-ZVktywq>VC_HGCKjCb+;j~> zmYk2U1S>Aa@P9R!@q;^Ww2)dlH~`Y(0c7wKy44+pUMKnubbxffH396bcWi&QP1=w> z5%*2WvlrOhqVtIa&2r-I7*+lmYGiZLq|q|+4dX25gwy#!%OM3<0>~M4kub2xNK*H; zD=79UPkCMwzOS$D)ut)jxca)?ysDN>7x`s0T%+<&d+cLKWb@42UxO0Os|I8`Hb7t# zJtwcd_5wyu-9lX{_l=RRsx#0uxMg!t;NW+;(<4NSFN3aTXP1bF&)XN1pZUblAj6^g z_XCgljc|S6qn=$Fux&5v|qbm+peH3~j|W7b8Q8UQ3WYT@R`ODmuiuV06NemJ)D z@J{aifsR=4qWNMEp99H(^=|>Bs|8CBGV~>*CM;SzjrqpC{47ncf$FaB6y4icfD`&4RzObNU6%F-k@6T@Hs`|6t(*u)xl+xkidMBu;#87YWaBT4sM-+mt!vGbKac-fCHhf?K)B*K>8g)7UcsKx zpS3aigzQ`inzRVeD@8jCsnb2W31YL_n2R{%h-sWE;f{Jcvp; zE+@BJYh+?{J_bwrk`=)09#GWn!U}MWr>*JucHJ{bGo0FgWx6uZkta*W5VPmyT2@Y> z!cUz)bZLmkVs(u~npWYSu#$6nJVy-DRkuuxj)56G%j@R|4U?wK=RD6sozVGA(5q)Ny+yuCnR!JUDNIIW)M3||ZVr)MM7`)u+s>i*=> zoS*EWY3h#t=$4OOFaULX43)W+>&EMvsC9{)`vp`|#$%#!B7iY3*ySBxlC=gjbj?>| znE3hl37}NH(ib_SjU~tv&U?fcI=q^LEb!^HVpbDkV@0F$eO)dU!eq zX?EG`t((aJ7WrNy!$?`VGsyS&=4I@{+M|K_DU57;sA`d}YBcwNwM(7D4pr7HZx8)#e&c=Bkom6g zsJgTG+9m2t-GJ>#quYZF?S&NEXU{svPBn=jsYLe$Ssa{WK^d?K=n!z119dSMp=)>V zCe0e*K8GW{4*pk}HZPxfdBy&5=6IC!JfQ2EJo0SVJKJ5djIZ7L#NZ@uAa-iE&^o-A z;eGtRVNgk%N9D+CYsHw5dYtF$xjki;0qWNd+so*b@UdEc*du~5;^VmTZ$ZZw?cv%8 z;GP32bpnG8{%QGHdjUtqZf>MRRhIk2C8ZvRAE2TArAvx&oQ~;M=>M>l!O2Pb<+2v$ zKA%Jtg|sw@&rw`mz;>Yl{X~3f%6SFRCPSa2BH)Vvn=w2m1q!nU*WT#W-icT`%u=GS z>=A{ju_>~8znuU|R#x;^E@nL&oDH%(^Ie`>v83z8N}@Mi;v!ZKawAz$0f53w;OGE6 zBnwR~l?WP3Ha(bH!c;$8xEv}y*zJf1aA8e11bbw$bF;*b1na57utzw2%t?s&b72dL zc2}?CR`UU;s$XV_vJ!+xJ8e|>QgnzL)E%v4b4PAK&IM}+_!U6(R)c7N!9rNr`VAwq zl%WFzXISg-5--^o3tQn4D&2>WZdiECtnmDyue2rJ8*h0K*g=608?ErDVt-h71d=bt zpN&T?z1U*0`Vu23ABiijm)r8P7MioF}0;uYA>l0EWM?6Ur332u5rDL|2~-fK`DReHA~7!vXbdB!eSK{8@nS;Vh^=&1hiQj^5uou5jbA@!6d- zjO-D`MLc(2Bz5MdD&DO6T-Cy-42#yMm)+m!II>^sE41+1S8qBTTC(U54Tgq}ce>}l zy&W0SDx$txychRxQJhHTJTnJ&WS1y*fl&fgR~L<+2`CS9HdfH#}TK;)@bbrlHRzi6u#-`YDb zx&uhh2j{RT1rV45u&DA-`A{k5-1cj}0;ugE8^A0%nqFYcK*^DzvAMb8(Z6~jDBu4T zt>r#004k^6{s$GGeX3b;UcMH4j%L0!wJ`k;nhE%VYv*!niIGVh%9#>ydFKS$<}VG) z^OCvmHHwUPIRxr1DCqNokbh6>f1?sP-2Wey09_>*B-A_3I-QSU^A%z}LLF8AqO|+9 zM`-CmsbU-lwkOuZMaGBTz|j4P@fJ%2IsRad^m(=Ae4Dxxe2_8PDSM1{C&nsOEkHzrMHC2{)oEJ4Jzpgb}NIDfim}a*o<%JQ6 zLmfvl`@gEI)>ke5)bZknBZmRVh*jozu^E^wV?)QWQ*E#T@1*SU+39qawfoYdya8&c zY4x(P&r!$mIR300=zJittgZem#<%bV3s9t2<4v&j*;Dgz0|bQzI)?|hPY>!37QBu? z_@T-h^k1l2`awzOa*gxW+JQ-MzQj@ZAs@$g$Aa(IM53 XKJ0LFz5q&Y$rR;PpBFqe`SAY$G75_2 literal 0 HcmV?d00001 diff --git a/planning/obstacle_avoidance_planner/media/mpt_fixed_traj.png b/planning/obstacle_avoidance_planner/media/mpt_fixed_traj.png new file mode 100644 index 0000000000000000000000000000000000000000..2344467811a836401be47740c68eb23b6cf33420 GIT binary patch literal 13916 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 literal 0 HcmV?d00001 diff --git a/planning/obstacle_avoidance_planner/media/mpt_traj.png b/planning/obstacle_avoidance_planner/media/mpt_traj.png new file mode 100644 index 0000000000000000000000000000000000000000..44a06bc1ef9f6d87ff2672cdda8453818f56f43a GIT binary patch literal 16265 zcmYj&2RK|&w>F7r5fQzG5YZxfHwXz)f&{_ny|>W@qa}Jz^r%5bAKmCgw8*Ft-Dsot z-phaF`|kbkJdbn6+0M4lK6|ab*1O&}{GIBX2ZXeQSXfvO6ct{pV`1SO1K%C-ae?Fa z>kM>OAE`*mamOlPQ=UG^A(n_&0n9A-l#z}U#HZ6m8{)$6S+CNY0jT+EXCF_ zGFTC5y-vH_E%OT_k=3u6y&iSCyDQ>`o z>=<&}5axHK0^HGuZgh=JPUFc(69FB_hACfMe#L>w;xk!B{yY8aB&7QxqjCXT5Q!

GFdJPH2y;oNTZqUm9zacth#7ok@dwepHg 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

;ncs;aniEcXmMv# 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

  • o$ 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Ÿ!ICu)y5pC-ln7iWTS! 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 +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace occupancy_grid_based_validator +{ +class OccupancyGridBasedValidator : public rclcpp::Node +{ +public: + explicit OccupancyGridBasedValidator(const rclcpp::NodeOptions & node_options); + +private: + rclcpp::Publisher::SharedPtr objects_pub_; + message_filters::Subscriber objects_sub_; + message_filters::Subscriber occ_grid_sub_; + tf2_ros::Buffer tf_buffer_; + tf2_ros::TransformListener tf_listener_; + + typedef message_filters::sync_policies::ApproximateTime< + autoware_auto_perception_msgs::msg::DetectedObjects, nav_msgs::msg::OccupancyGrid> + SyncPolicy; + typedef message_filters::Synchronizer Sync; + Sync sync_; + float mean_threshold_; + bool enable_debug_; + + void onObjectsAndOccGrid( + const autoware_auto_perception_msgs::msg::DetectedObjects::ConstSharedPtr & input_objects, + const nav_msgs::msg::OccupancyGrid::ConstSharedPtr & input_occ_grid); + + cv::Mat fromOccupancyGrid(const nav_msgs::msg::OccupancyGrid & occupancy_grid); + std::optional getMask( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + const autoware_auto_perception_msgs::msg::DetectedObject & object); + std::optional getMask( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + const autoware_auto_perception_msgs::msg::DetectedObject & object, cv::Mat mask); + void toPolygon2d( + const autoware_auto_perception_msgs::msg::DetectedObject & object, + std::vector & vertices); + void showDebugImage( + const nav_msgs::msg::OccupancyGrid & ros_occ_grid, + const autoware_auto_perception_msgs::msg::DetectedObjects & objects, const cv::Mat & occ_grid); +}; +} // namespace occupancy_grid_based_validator + +#endif // OCCUPANCY_GRID_BASED_VALIDATOR__OCCUPANCY_GRID_BASED_VALIDATOR_HPP_ diff --git a/perception/detected_object_validation/launch/occupancy_grid_based_validator.launch.xml b/perception/detected_object_validation/launch/occupancy_grid_based_validator.launch.xml new file mode 100644 index 0000000000000..542f7a5c4463f --- /dev/null +++ b/perception/detected_object_validation/launch/occupancy_grid_based_validator.launch.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/perception/detected_object_validation/package.xml b/perception/detected_object_validation/package.xml new file mode 100644 index 0000000000000..a29206a0d7489 --- /dev/null +++ b/perception/detected_object_validation/package.xml @@ -0,0 +1,29 @@ + + + detected_object_validation + 1.0.0 + The ROS2 detected_object_validation package + Yukihiro Saito + Apache License 2.0 + + ament_cmake_auto + eigen3_cmake_module + + autoware_auto_perception_msgs + message_filters + nav_msgs + rclcpp + rclcpp_components + tf2 + tf2_geometry_msgs + tf2_ros + tier4_autoware_utils + tier4_perception_msgs + + ament_lint_auto + autoware_lint_common + + + ament_cmake + + diff --git a/perception/detected_object_validation/src/occupancy_grid_based_validator.cpp b/perception/detected_object_validation/src/occupancy_grid_based_validator.cpp new file mode 100644 index 0000000000000..bc7788ee2af32 --- /dev/null +++ b/perception/detected_object_validation/src/occupancy_grid_based_validator.cpp @@ -0,0 +1,258 @@ +// Copyright 2022 Tier IV, Inc. +// +// 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 "occupancy_grid_based_validator/occupancy_grid_based_validator.hpp" + +#include + +#include + +#include + +#define EIGEN_MPL2_ONLY +#include +#include + +namespace +{ +boost::optional getTransform( + const tf2_ros::Buffer & tf_buffer, const std::string & source_frame_id, + const std::string & target_frame_id, const rclcpp::Time & time) +{ + try { + geometry_msgs::msg::TransformStamped self_transform_stamped; + self_transform_stamped = tf_buffer.lookupTransform( + /*target*/ target_frame_id, /*src*/ source_frame_id, time, + rclcpp::Duration::from_seconds(0.5)); + return self_transform_stamped.transform; + } catch (tf2::TransformException & ex) { + RCLCPP_WARN_STREAM(rclcpp::get_logger("multi_object_tracker"), ex.what()); + return boost::none; + } +} + +bool transformDetectedObjects( + const autoware_auto_perception_msgs::msg::DetectedObjects & input_msg, + const std::string & target_frame_id, const tf2_ros::Buffer & tf_buffer, + autoware_auto_perception_msgs::msg::DetectedObjects & output_msg) +{ + output_msg = input_msg; + + /* transform to world coordinate */ + if (input_msg.header.frame_id != target_frame_id) { + output_msg.header.frame_id = target_frame_id; + tf2::Transform tf_target2objects_world; + tf2::Transform tf_target2objects; + tf2::Transform tf_objects_world2objects; + { + const auto ros_target2objects_world = + getTransform(tf_buffer, input_msg.header.frame_id, target_frame_id, input_msg.header.stamp); + if (!ros_target2objects_world) { + return false; + } + tf2::fromMsg(*ros_target2objects_world, tf_target2objects_world); + } + for (size_t i = 0; i < output_msg.objects.size(); ++i) { + tf2::fromMsg( + output_msg.objects.at(i).kinematics.pose_with_covariance.pose, tf_objects_world2objects); + tf_target2objects = tf_target2objects_world * tf_objects_world2objects; + tf2::toMsg(tf_target2objects, output_msg.objects.at(i).kinematics.pose_with_covariance.pose); + // Note: Covariance is not transformed. + } + } + return true; +} + +} // namespace + +namespace occupancy_grid_based_validator +{ +using Label = autoware_auto_perception_msgs::msg::ObjectClassification; +using Shape = autoware_auto_perception_msgs::msg::Shape; + +OccupancyGridBasedValidator::OccupancyGridBasedValidator(const rclcpp::NodeOptions & node_options) +: rclcpp::Node("occupancy_grid_based_validator", node_options), + objects_sub_(this, "~/input/detected_objects", rclcpp::QoS{1}.get_rmw_qos_profile()), + occ_grid_sub_(this, "~/input/occupancy_grid_map", rclcpp::QoS{1}.get_rmw_qos_profile()), + tf_buffer_(get_clock()), + tf_listener_(tf_buffer_), + sync_(SyncPolicy(10), objects_sub_, occ_grid_sub_) +{ + using std::placeholders::_1; + using std::placeholders::_2; + sync_.registerCallback( + std::bind(&OccupancyGridBasedValidator::onObjectsAndOccGrid, this, _1, _2)); + objects_pub_ = create_publisher( + "~/output/objects", rclcpp::QoS{1}); + + mean_threshold_ = declare_parameter("mean_threshold", 0.6); + enable_debug_ = declare_parameter("enable_debug", false); +} + +void OccupancyGridBasedValidator::onObjectsAndOccGrid( + const autoware_auto_perception_msgs::msg::DetectedObjects::ConstSharedPtr & input_objects, + const nav_msgs::msg::OccupancyGrid::ConstSharedPtr & input_occ_grid) +{ + autoware_auto_perception_msgs::msg::DetectedObjects output; + output.header = input_objects->header; + + // Transform to occ grid frame + autoware_auto_perception_msgs::msg::DetectedObjects transformed_objects; + if (!transformDetectedObjects( + *input_objects, input_occ_grid->header.frame_id, tf_buffer_, transformed_objects)) + return; + + // Convert ros data type to cv::Mat + cv::Mat occ_grid = fromOccupancyGrid(*input_occ_grid); + + // Get vehicle mask image and calculate mean within mask. + for (size_t i = 0; i < transformed_objects.objects.size(); ++i) { + const auto & transformed_object = transformed_objects.objects.at(i); + const auto & object = input_objects->objects.at(i); + const auto & label = object.classification.front().label; + const bool is_vehicle = Label::CAR == label || Label::TRUCK == label || Label::BUS == label || + Label::TRAILER == label; + if (is_vehicle) { + auto mask = getMask(*input_occ_grid, transformed_object); + const float mean = mask ? cv::mean(occ_grid, mask.value())[0] * 0.01 : 1.0; + if (mean_threshold_ < mean) output.objects.push_back(object); + } else { + output.objects.push_back(object); + } + } + + objects_pub_->publish(output); + + if (enable_debug_) showDebugImage(*input_occ_grid, transformed_objects, occ_grid); +} + +std::optional OccupancyGridBasedValidator::getMask( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + const autoware_auto_perception_msgs::msg::DetectedObject & object) +{ + cv::Mat mask = cv::Mat::zeros(occupancy_grid.info.height, occupancy_grid.info.width, CV_8UC1); + return getMask(occupancy_grid, object, mask); +} + +std::optional OccupancyGridBasedValidator::getMask( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + const autoware_auto_perception_msgs::msg::DetectedObject & object, cv::Mat mask) +{ + const auto & resolution = occupancy_grid.info.resolution; + const auto & origin = occupancy_grid.info.origin; + std::vector vertices; + std::vector pixel_vertices; + toPolygon2d(object, vertices); + + bool is_polygon_within_image = true; + for (const auto & vertex : vertices) { + const float px = (vertex.x - origin.position.x) / resolution; + const float py = (vertex.y - origin.position.y) / resolution; + const bool is_point_within_image = (0 <= px && px < mask.cols && 0 <= py && py < mask.rows); + + if (!is_point_within_image) is_polygon_within_image = false; + + pixel_vertices.push_back(cv::Point2f(px, py)); + } + + if (is_polygon_within_image && !pixel_vertices.empty()) { + cv::fillConvexPoly(mask, pixel_vertices, cv::Scalar(255)); + return mask; + } else { + return std::nullopt; + } +} + +cv::Mat OccupancyGridBasedValidator::fromOccupancyGrid( + const nav_msgs::msg::OccupancyGrid & occupancy_grid) +{ + cv::Mat cv_occ_grid = + cv::Mat::zeros(occupancy_grid.info.height, occupancy_grid.info.width, CV_8UC1); + for (size_t i = 0; i < occupancy_grid.data.size(); ++i) { + size_t y = i / occupancy_grid.info.width; + size_t x = i % occupancy_grid.info.width; + const auto & data = occupancy_grid.data[i]; + cv_occ_grid.at(y, x) = + std::min(std::max(data, static_cast(0)), static_cast(50)) * 2; + } + return cv_occ_grid; +} + +void OccupancyGridBasedValidator::toPolygon2d( + const autoware_auto_perception_msgs::msg::DetectedObject & object, + std::vector & vertices) +{ + if (object.shape.type == Shape::BOUNDING_BOX) { + const auto & pose = object.kinematics.pose_with_covariance.pose; + double yaw = tier4_autoware_utils::normalizeRadian(tf2::getYaw(pose.orientation)); + Eigen::Matrix2d rotation; + rotation << std::cos(yaw), -std::sin(yaw), std::sin(yaw), std::cos(yaw); + Eigen::Vector2d offset0, offset1, offset2, offset3; + offset0 = rotation * + Eigen::Vector2d(object.shape.dimensions.x * 0.5f, object.shape.dimensions.y * 0.5f); + offset1 = rotation * + Eigen::Vector2d(object.shape.dimensions.x * 0.5f, -object.shape.dimensions.y * 0.5f); + offset2 = rotation * + Eigen::Vector2d(-object.shape.dimensions.x * 0.5f, -object.shape.dimensions.y * 0.5f); + offset3 = rotation * + Eigen::Vector2d(-object.shape.dimensions.x * 0.5f, object.shape.dimensions.y * 0.5f); + vertices.push_back(cv::Point2f(pose.position.x + offset0.x(), pose.position.y + offset0.y())); + vertices.push_back(cv::Point2f(pose.position.x + offset1.x(), pose.position.y + offset1.y())); + vertices.push_back(cv::Point2f(pose.position.x + offset2.x(), pose.position.y + offset2.y())); + vertices.push_back(cv::Point2f(pose.position.x + offset3.x(), pose.position.y + offset3.y())); + } else if (object.shape.type == Shape::CYLINDER) { + RCLCPP_WARN_THROTTLE( + this->get_logger(), *this->get_clock(), 5, "CYLINDER type is not supported"); + } else if (object.shape.type == Shape::POLYGON) { + RCLCPP_WARN_THROTTLE( + this->get_logger(), *this->get_clock(), 5, "POLYGON type is not supported"); + } +} + +void OccupancyGridBasedValidator::showDebugImage( + const nav_msgs::msg::OccupancyGrid & ros_occ_grid, + const autoware_auto_perception_msgs::msg::DetectedObjects & objects, const cv::Mat & occ_grid) +{ + cv::namedWindow("removed_objects_image", cv::WINDOW_NORMAL); + cv::namedWindow("passed_objects_image", cv::WINDOW_NORMAL); + cv::Mat removed_objects_image = occ_grid.clone(); + cv::Mat passed_objects_image = occ_grid.clone(); + + // Get vehicle mask image and calculate mean within mask. + for (size_t i = 0; i < objects.objects.size(); ++i) { + const auto & object = objects.objects.at(i); + const auto & label = object.classification.front().label; + const bool is_vehicle = Label::CAR == label || Label::TRUCK == label || Label::BUS == label || + Label::TRAILER == label; + if (is_vehicle) { + auto mask = getMask(ros_occ_grid, object); + const float mean = mask ? cv::mean(occ_grid, mask.value())[0] * 0.01 : 1.0; + if (mean_threshold_ < mean) { + auto mask = getMask(ros_occ_grid, object, passed_objects_image); + if (mask) passed_objects_image = mask.value(); + } else { + auto mask = getMask(ros_occ_grid, object, removed_objects_image); + if (mask) removed_objects_image = mask.value(); + } + } + } + cv::imshow("removed_objects_image", removed_objects_image); + cv::imshow("passed_objects_image", passed_objects_image); + cv::waitKey(2); +} + +} // namespace occupancy_grid_based_validator + +#include +RCLCPP_COMPONENTS_REGISTER_NODE(occupancy_grid_based_validator::OccupancyGridBasedValidator) diff --git a/perception/detection_by_tracker/package.xml b/perception/detection_by_tracker/package.xml index 0c969ba0077af..1c4dc51cc7084 100644 --- a/perception/detection_by_tracker/package.xml +++ b/perception/detection_by_tracker/package.xml @@ -17,6 +17,7 @@ rclcpp_components shape_estimation tf2 + tf2_geometry_msgs tf2_ros tier4_autoware_utils tier4_perception_msgs diff --git a/sensing/laserscan_to_occupancy_grid_map/src/laserscan_to_occupancy_grid_map_node.cpp b/sensing/laserscan_to_occupancy_grid_map/src/laserscan_to_occupancy_grid_map_node.cpp index c82df6cccedbb..167947c9ff4f7 100644 --- a/sensing/laserscan_to_occupancy_grid_map/src/laserscan_to_occupancy_grid_map_node.cpp +++ b/sensing/laserscan_to_occupancy_grid_map/src/laserscan_to_occupancy_grid_map_node.cpp @@ -121,6 +121,7 @@ OccupancyGridMapNode::OccupancyGridMapNode(const rclcpp::NodeOptions & node_opti use_height_filter_ = declare_parameter("use_height_filter", true); enable_single_frame_mode_ = declare_parameter("enable_single_frame_mode", false); const double map_length{declare_parameter("map_length", 100.0)}; + const double map_width{declare_parameter("map_width", 100.0)}; const double map_resolution{declare_parameter("map_resolution", 0.5)}; const bool input_obstacle_pointcloud{declare_parameter("input_obstacle_pointcloud", true)}; const bool input_obstacle_and_raw_pointcloud{ @@ -152,7 +153,7 @@ OccupancyGridMapNode::OccupancyGridMapNode(const rclcpp::NodeOptions & node_opti /* Occupancy grid */ occupancy_grid_map_updater_ptr_ = std::make_shared( - map_length / map_resolution, map_length / map_resolution, map_resolution); + map_length / map_resolution, map_width / map_resolution, map_resolution); } PointCloud2::SharedPtr OccupancyGridMapNode::convertLaserscanToPointCLoud2( From e4d15ee3580083a886bbf3357a1fdcdc711f9a1f Mon Sep 17 00:00:00 2001 From: melike <41450930+meliketanrikulu@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:55:25 +0300 Subject: [PATCH 16/16] docs(ekf_localizer): update subscribed topic msg type (#550) Signed-off-by: melike --- localization/ekf_localizer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/ekf_localizer/README.md b/localization/ekf_localizer/README.md index e729845a4ba7c..5f02f35bab1a0 100644 --- a/localization/ekf_localizer/README.md +++ b/localization/ekf_localizer/README.md @@ -45,7 +45,7 @@ The parameters and input topic names can be set in the `ekf_localizer.launch` fi Input pose source with measurement covariance matrix. -- measured_twist_with_covariance (geometry_msgs/PoseWithCovarianceStamped) +- measured_twist_with_covariance (geometry_msgs/TwistWithCovarianceStamped) Input twist source with measurement covariance matrix.
  • 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 literal 0 HcmV?d00001 diff --git a/planning/obstacle_avoidance_planner/media/output_traj.png b/planning/obstacle_avoidance_planner/media/output_traj.png new file mode 100644 index 0000000000000000000000000000000000000000..e4cfae92d0a8f19d9a665279d4e84e5326776bec GIT binary patch literal 17862 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 literal 0 HcmV?d00001 diff --git a/planning/obstacle_avoidance_planner/obstacle_avoidance_planner-design.ja.md b/planning/obstacle_avoidance_planner/obstacle_avoidance_planner-design.ja.md new file mode 100644 index 0000000000000..8610070c4871d --- /dev/null +++ b/planning/obstacle_avoidance_planner/obstacle_avoidance_planner-design.ja.md @@ -0,0 +1,330 @@ +# Obstacle Avoidance Planner + +## Purpose + +obstacle_avoidance_planner は入力された path と drivable area、および動物体情報をもとに、車両キネマティクスモデルを考慮して車が走行可能な軌道を生成する。trajectory 内の各経路点の位置姿勢のみ計画しており、速度や加速度の計算は後段の別モジュールで行われる。 + + + +## Inputs / Outputs + +### input + +- reference_path [`autoware_auto_planning_msgs/Path`] : Reference path and the corresponding drivable area. +- objects [`autoware_auto_perception_msgs/PredictedObjects`] : Recognized objects around the vehicle + +### output + +- optimized_trajectory [`autoware_auto_planning_msgs/Trajectory`] : Optimized trajectory that is feasible to drive and collision-free. + +## Flowchart + +フローチャートとともに、各機能の概要をおおまかに説明する。 + +![obstacle_avoidance_planner_flowchart](./media/obstacle_avoidance_planner_flowchart.drawio.svg) + +### Manage trajectory generation + +以下の条件のいずれかが満たされたときに、経路生成の関数を呼ぶ。それ以外の時は前回生成された経路を出力する。 + +- 前回経路生成時から一定距離走行 (default: 10.0 [m]) +- 一定時間経過 (default: 1.0 [s]) +- レーンチェンジなどで入力の path の形状が変わった時 +- 自車位置が前回経路から大きく外れた時 + +入力の path の形状が変わった時と自車が前回経路から大きく外れた時は、保持している前回経路を破棄するリセット機能もある。 + +### Select objects to avoid + +静的で路肩にある障害物のみ回避対象とする。 +具体的には、以下の 3 つの条件を満たすものであり、図で示すと赤い id: 3, 4, 5 の物体である。 + +- 指定された速度以下 (default: 0.1 [m/s]) +- 物体重心が center line 上に存在しない(前車追従の車を避けないようにするため) +- 少なくとも 1 つ以上の物体形状ポリゴン点が drivable area に存在する。 + +![obstacle_to_avoid](./media/obstacle_to_avoid.drawio.svg) + +### Generate object costmap + +回避対象である障害物からの距離に応じたコストマップを生成する。 + +### Generate road boundary costmap + +道路からの距離に応じたコストマップを生成する。 + +これらのコストマップは後段の Optimize trajectory の手法である Model predictive trajectory の障害物・道路境界に衝突しない制約条件を定式化する際に使用される。 + +### Smooth path + +後段の最適化処理で曲率のなめらかな参照経路が必要であるため、最適化前に path をなめらかにして trajectory の形式で出力する。 +この平滑化は障害物を考慮しない。 + +### Optimize trajectory + +参照経路に基づいたフレネ座標系で車両キネマティクス、及び追従誤差を定義し、二次計画法を用いて車両キネマティクスや障害物回避を考慮しながら追従誤差が小さくなるように経路生成する。 +自車近傍の経路の急な変化を防ぐため、直近の経路は前回の経路をそのまま使用する。 +計算コストを抑えるため、最終的に出力する経路長よりも短い距離に対してのみ計算を行う。 + +### Extend trajectory + +経路を規定の長さ(default: 200m)に拡張するため、最適化した経路の先を Reference 経路と接続する。 + +### Check drivable area + +生成された経路が走行可能領域を逸脱していた場合、前回生成された走行可能領域内にある経路を出力する。 + +_Rationale_ +現在の設計において、数値誤差による破綻を防ぐために障害物回避は全てソフト制約として考慮されており、生成された経路に置いて車両が走行可能領域内に入っているかの判定が必要。 + +### Apply path velocity + +入力の path に埋め込まれている速度を出力される trajectory に埋め込む。経路間隔が異なるため線形補間を用いる。 + +## Algorithms + +Smooth path で使われている Elastic band と、Optimized trajectory で使われている Model predictive trajectory の詳細な説明をする。 + +### Elastic band + +#### 概要 + +behavior_path_planner で計算された path は場合によってはなめらかではない可能性があるので、その path の平滑化をすることが目的である。 + +次の Model predictive trajectory でも平滑化項は入っているが、目標経路になるべく追従しようとする項も入っているため、目標経路がなめらかでなかった場合はこの 2 つの項が反発する。 +それを防ぐためにここで平滑化のみを行っている。 +また Model predictive trajectory では各点における曲率と法線を元に最適化しており、平滑化された目標経路を渡すことで最適化の結果を安定させる狙いもある。 + +平滑化の際、障害物や道路壁を考慮していないため障害物や道路壁に衝突した trajectory が計算されることもある。 + +この Elastic band と次の Model predictive trajectory は、計算負荷を抑えるためにある程度の長さでクリップした trajectory を出力するようになっている。 + +#### 数式 + +前後の点からの距離の差の二乗を目的関数とする二次計画。 + +各点は一定の範囲以内しか動かないという制約を設けることで、入力の軌道をよりなめらかにした軌道を得る。 + +$\boldsymbol{p}_k$を$k$番目の経路点の座標ととしたとき以下のコスト関数を最小化する二次計画を解く。ここでは始点と終点である$\boldsymbol{p}_0$と$\boldsymbol{p}_n$は固定である。 + +$$ +\min \sum_{k=1}^{n-1} |\boldsymbol{p}_{k+1} - \boldsymbol{p}_{k}| - |\boldsymbol{p}_{k} - \boldsymbol{p}_{k-1}| +$$ + +### Model predictive trajectory + +#### 概要 + +Elastic band で平滑化された trajectory に対して、以下の条件を満たすように修正を行うことが目的である。 + +- 線形化された車両のキネマティクスモデルに基づき走行可能である +- 障害物や道路壁面に衝突しない + +障害物や道路壁面に衝突しない条件はハードではなくソフト制約として含まれている。車両の後輪位置、前輪位置、その中心位置において障害物・道路境界との距離から制約条件が計算されている。 +条件を満たさない解が出力された場合は、後段の後処理で弾かれ、前の周期で計画された trajectory を出力する。 + +自車付近の経路が振動しないように、自車近傍の経路点を前回の経路点と一致させる制約条件も含まれており、これが唯一の二次計画法のハード制約である。 + +#### 数式 + +以下のように、経路に対して車両が追従するときの bicycle kinematics model を考える。 +時刻$k$における、経路上の車両の最近傍点の座標($x$座標は経路の接線に平行)から見た追従誤差に関して、横偏差$y_k$、向きの偏差$\theta_k$、ステア角$\delta_k$と定める。 + +![vehicle_error_kinematics](./media/vehicle_error_kinematics.png) + +指令ステア角度を$\delta_{des, k}$とすると、ステア角の遅延を考慮した車両キネマティクスモデルは以下で表される。この時、ステア角$\delta_k$は一次遅れ系として指令ステア角に追従すると仮定する。 + +$$ +\begin{align} +y_{k+1} & = y_{k} + v \sin \theta_k dt \\ +\theta_{k+1} & = \theta_k + \frac{v \tan \delta_k}{L}dt - \kappa_k v \cos \theta_k dt \\ +\delta_{k+1} & = \delta_k - \frac{\delta_k - \delta_{des,k}}{\tau}dt +\end{align} +$$ + +次にこれらの式を線形化する。$y_k$, $\theta_k$は追従誤差であるため微小近似でき、$\sin \theta_k \approx \theta_k$となる。 + +$\delta_k$に関してはステア角であるため微小とみなせない。そこで、以下のように参照経路の曲率$\kappa_k$から計算されるステア角$\delta_{\mathrm{ref}, k}$を用いることにより、$\delta_k$を微小な値$\Delta \delta_k$で表す。 + +ここで注意すべきこととしては、$\delta_k$は最大ステア角度$\delta_{\max}$以内の値を取る。曲率$\kappa_k$から計算された$\delta_{\mathrm{ref}, k}$が最大ステア角度$\delta_{\max}$より大きいときに$\delta_{\mathrm{ref}, k}$をそのまま使用して線形化すると、$\Delta \delta_k = \delta - \delta_{\mathrm{ref}, k} = \delta_{\max} - \delta_{\mathrm{ref}, k}$となり、$\Delta \delta_k$の絶対値が大きくなる。すなわち、$\delta_{\mathrm{ref}, k}$にも最大ステア角度制限を適用する必要がある。 + +$$ +\begin{align} +\delta_{\mathrm{ref}, k} & = \mathrm{clamp}(\arctan(L \kappa_k), -\delta_{\max}, \delta_{\max}) \\ +\delta_k & = \delta_{\mathrm{ref}, k} + \Delta \delta_k, \ \Delta \delta_k \ll 1 \\ +\end{align} +$$ + +$\mathrm{clamp}(v, v_{\min}, v_{\max})$は$v$を$v_{\min}$から$v_{\max}$の範囲内に丸める関数である。 + +この$\delta_{\mathrm{ref}, k}$を介して$\tan \delta_k$を線形な式で近似する。 + +$$ +\begin{align} +\tan \delta_k & \approx \tan \delta_{\mathrm{ref}, k} + \left.\frac{d \tan \delta}{d \delta}\right|_{\delta = \delta_{\mathrm{ref}, k}} \Delta \delta_k \\ +& = \tan \delta_{\mathrm{ref}, k} + \left.\frac{d \tan \delta}{d \delta}\right|_{\delta = \delta_{\mathrm{ref}, k}} (\delta_{\mathrm{ref}, k} - \delta_k) \\ +& = \tan \delta_{\mathrm{ref}, k} - \frac{\delta_{\mathrm{ref}, k}}{\cos^2 \delta_{\mathrm{ref}, k}} + \frac{1}{\cos^2 \delta_{\mathrm{ref}, k}} \delta_k +\end{align} +$$ + +以上の線形化を踏まえ、誤差ダイナミクスは以下のように線形な行列演算で記述できる。 + +$$ +\begin{align} + \begin{pmatrix} + y_{k+1} \\ + \theta_{k+1} \\ + \delta_{k+1} + \end{pmatrix} + = + \begin{pmatrix} + 1 & v dt & 0 \\ + 0 & 1 & \frac{v dt}{L \cos^{2} \delta_{\mathrm{ref}, k}} \\ + 0 & 0 & 1 - \frac{dt}{\tau} + \end{pmatrix} + \begin{pmatrix} + y_k \\ + \theta_k \\ + \delta_k + \end{pmatrix} + + + \begin{pmatrix} + 0 \\ + 0 \\ + \frac{dt}{\tau} + \end{pmatrix} + \delta_{des} + + + \begin{pmatrix} + 0 \\ + \frac{v \tan(\delta_{\mathrm{ref}, k}) dt}{L} - \frac{v \delta_{\mathrm{ref}, k} dt}{L \cos^{2} \delta_{\mathrm{ref}, k}} - \kappa_k v dt\\ + 0 + \end{pmatrix} +\end{align} +$$ + +平滑化と経路追従のための目的関数は以下で表され、 + +$$ +\begin{align} +J_1 & (y_{0...N-1}, \theta_{0...N-1}, \delta_{0...N-1}) \\ & = w_y \sum_{k} y_k^2 + w_{\theta} \sum_{k} \theta_k^2 + w_{\delta} \sum_k \delta_k^2 + w_{\dot{\delta}} \sum_k \dot{\delta}_k^2 + w_{\ddot{\delta}} \sum_k \ddot{\delta}_k^2 +\end{align} +$$ + +前述の通り、車両が障害物・道路境界に侵入しない条件はスラック変数を用いてソフト制約として表されている。 +車両の後輪位置、前輪位置、およびその中心位置における障害物・道路境界までの距離をそれぞれ$y_{\mathrm{base}, k}, y_{\mathrm{top}, k}, y_{\mathrm{mid}, k}$とする。 +ここでそれぞれに対するスラック変数 $\lambda_{\mathrm{base}}, \lambda_{\mathrm{top}}, \lambda_{\mathrm{mid}}$を定義し、 + +$$ +y_{\mathrm{base}, k, \min} - \lambda_{\mathrm{base}, k} \leq y_{\mathrm{base}, k} (y_k) \leq y_{\mathrm{base}, k, \max} + \lambda_{\mathrm{base}, k}\\ +y_{\mathrm{top}, k, \min} - \lambda_{\mathrm{top}, k} \leq y_{\mathrm{top}, k} (y_k) \leq y_{\mathrm{top}, k, \max} + \lambda_{\mathrm{top}, k}\\ +y_{\mathrm{mid}, k, \min} - \lambda_{\mathrm{mid}, k} \leq y_{\mathrm{mid}, k} (y_k) \leq y_{\mathrm{mid}, k, \max} + \lambda_{\mathrm{mid}, k} +$$ + +$y_{\mathrm{base}, k}, y_{\mathrm{top}, k}, y_{\mathrm{mid}, k}$は$y_k$の 1 次式として表現できるので、このソフト制約の目的関数は、以下のように記述できる。 + +$$ +\begin{align} +J_2 & (\lambda_{\mathrm{base}, 0...N-1}, \lambda_{\mathrm{mid}, 0...N-1}, \lambda_{\mathrm{top}, 0...N-1}) \\ & = w_{\mathrm{base}} \sum_{k} \lambda_{\mathrm{base}, k}^2 + w_{\mathrm{mid}} \sum_k \lambda_{\mathrm{mid}, k}^2 + w_{\mathrm{top}} \sum_k \lambda_{\mathrm{top}, k}^2 +\end{align} +$$ + +スラック変数も二次計画法の設計変数となり、全ての設計変数をまとめたベクトル$\boldsymbol{x}$を定義する。 + +$$ +\begin{align} +\boldsymbol{x} = +\begin{pmatrix} +... & y_k & \lambda_{\mathrm{base}, k} & \lambda_{\mathrm{top}, k} & \lambda_{\mathrm{mid}, k} & ... +\end{pmatrix}^T +\end{align} +$$ + +これらの 2 つの目的関数の和を目的関数とする。 + +$$ +\begin{align} +\min_{\boldsymbol{x}} J (\boldsymbol{x}) = \min_{\boldsymbol{x}} J_1 & (y_{0...N-1}, \theta_{0...N-1}, \delta_{0...N-1}) + J_2 (\lambda_{\mathrm{base}, 0...N-1}, \lambda_{\mathrm{mid}, 0...N-1}, \lambda_{\mathrm{top}, 0...N-1}) +\end{align} +$$ + +前述の通り、真にハードな制約条件は車両前方ある程度の距離$N_{fix}$までの経路点の状態は前回値になるような条件であり、以下のように記述できる。 + +$$ +\begin{align} +\delta_k = \delta_{k}^{\mathrm{prev}} (0 \leq i \leq N_{\mathrm{fix}}) +\end{align} +$$ + +であり、これらを以下のような二次計画法の係数行列に変換して二次計画法を解く + +$$ +\begin{align} +\min_{\boldsymbol{x}} \ & \frac{1}{2} \boldsymbol{x}^T \boldsymbol{P} \boldsymbol{x} + \boldsymbol{q} \boldsymbol{x} \\ +\mathrm{s.t.} \ & \boldsymbol{b}_l \leq \boldsymbol{A} \boldsymbol{x} \leq \boldsymbol{b}_u +\end{align} +$$ + +## Limitation + +- カーブ時に外側に膨らんだ経路を返す +- behavior_path_planner と obstacle_avoidance_planner の経路計画の役割分担がはっきりと決まっていない + - behavior_path_planner 側で回避する場合と、obstacle_avoidance_planner で回避する場合がある +- behavior_path_planner から来る path が道路壁に衝突していると、大きく外側に膨れた trajectory を計画する (柏の葉のカーブで顕著) +- 計算負荷が高い + +## How to debug + +obstacle_avoidance_planner` から出力される debug 用の topic について説明する。 + +- **interpolated_points_marker** + - obstacle avoidance planner への入力経路をリサンプルしたもの。この経路が道路内に収まっているか(道路内にあることが必須ではない)、十分になめらかか(ある程度滑らかでないとロジックが破綻する)、などを確認する。 + +![interpolated_points_marker](./media/interpolated_points_marker.png) + +- **smoothed_points_text** + - Elastic Band の計算結果。点群ではなく小さい数字が描画される。平滑化されたこの経路が道路内からはみ出ていないか、歪んでいないかなどを確認。 + +![smoothed_points_text](./media/smoothed_points_text.png) + +- **(base/top/mid)\_bounds_line** + - 壁との衝突判定における横方向の道路境界までの距離(正確には - vehicle_width / 2.0)。 + - 車両の 3 箇所(base, top, mid)で衝突判定を行っており、3 つのマーカーが存在する。 + - 黄色い線の各端点から道路境界までの距離が車幅の半分くらいであれば異常なし(ここがおかしい場合はロジック異常)。 + +![bounds_line](./media/bounds_line.png) + +- **optimized_points_marker** + - MPT の計算結果。道路からはみ出ていないか、振動していないかなどを確認 + +![optimized_points_marker](./media/optimized_points_marker.png) + +- **Trajectory with footprint** + - TrajectoryFootprint の rviz_plugin を用いて経路上の footprint を描画することが可能。これを用いて obstacle_avoidance_planner への入出力の footprint、経路に収まっているかどうか等を確認する。 + +![trajectory_with_footprint](./media/trajectory_with_footprint.png) + +- **Drivable Area** + - obstacle avoidance への入力の走行可能領域を表示する。Drivable Area の生成に不具合があると生成経路が歪む可能性がある。 + - topic 名:`/planning/scenario_planning/lane_driving/behavior_planning/behavior_path_planner/debug/drivable_area` + - `nav_msgs/msg/OccupancyGrid` 型として出力される + +![drivable_area](./media/drivable_area.png) + +- **area_with_objects** + - 入力された走行可能領域から障害物を取り除いた後の、走行可能領域 + - `nav_msgs/msg/OccupancyGrid` 型として出力される + +![area_with_objects](./media/area_with_objects.png) + +- **object_clearance_map** + - 回避対象の障害物からの距離を可視化したもの。 + - `nav_msgs/msg/OccupancyGrid` 型として出力される + +![object_clearance_map](./media/object_clearance_map.png) + +- **clearance_map** + - 入力された走行可能領域からの距離を可視化したもの。 + - `nav_msgs/msg/OccupancyGrid` 型として出力される + +![clearance_map](./media/clearance_map.png) diff --git a/planning/obstacle_avoidance_planner/package.xml b/planning/obstacle_avoidance_planner/package.xml index b76c6db96f215..c58292c667327 100644 --- a/planning/obstacle_avoidance_planner/package.xml +++ b/planning/obstacle_avoidance_planner/package.xml @@ -21,6 +21,7 @@ std_msgs tf2_ros tier4_autoware_utils + tier4_debug_msgs tier4_planning_msgs vehicle_info_util visualization_msgs diff --git a/planning/obstacle_avoidance_planner/src/costmap_generator.cpp b/planning/obstacle_avoidance_planner/src/costmap_generator.cpp new file mode 100644 index 0000000000000..dafc2d5881b3e --- /dev/null +++ b/planning/obstacle_avoidance_planner/src/costmap_generator.cpp @@ -0,0 +1,342 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 "obstacle_avoidance_planner/costmap_generator.hpp" + +#include "obstacle_avoidance_planner/cv_utils.hpp" +#include "obstacle_avoidance_planner/utils.hpp" +#include "tier4_autoware_utils/tier4_autoware_utils.hpp" + +namespace +{ +cv::Point toCVPoint(const geometry_msgs::msg::Point & p) +{ + cv::Point cv_point; + cv_point.x = p.x; + cv_point.y = p.y; + return cv_point; +} + +bool isAvoidingObjectType( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const TrajectoryParam & traj_param) +{ + if ( + (object.classification.at(0).label == object.classification.at(0).UNKNOWN && + traj_param.is_avoiding_unknown) || + (object.classification.at(0).label == object.classification.at(0).CAR && + traj_param.is_avoiding_car) || + (object.classification.at(0).label == object.classification.at(0).TRUCK && + traj_param.is_avoiding_truck) || + (object.classification.at(0).label == object.classification.at(0).BUS && + traj_param.is_avoiding_bus) || + (object.classification.at(0).label == object.classification.at(0).BICYCLE && + traj_param.is_avoiding_bicycle) || + (object.classification.at(0).label == object.classification.at(0).MOTORCYCLE && + traj_param.is_avoiding_motorbike) || + (object.classification.at(0).label == object.classification.at(0).PEDESTRIAN && + traj_param.is_avoiding_pedestrian)) { + return true; + } + return false; +} + +bool arePointsInsideDriveableArea( + const std::vector & image_points, const cv::Mat & clearance_map) +{ + bool points_inside_area = false; + for (const auto & image_point : image_points) { + const float clearance = + clearance_map.ptr(static_cast(image_point.y))[static_cast(image_point.x)]; + if (clearance > 0) { + points_inside_area = true; + } + } + return points_inside_area; +} + +bool isAvoidingObject( + const PolygonPoints & polygon_points, + const autoware_auto_perception_msgs::msg::PredictedObject & object, const cv::Mat & clearance_map, + const nav_msgs::msg::MapMetaData & map_info, + const std::vector & path_points, + const TrajectoryParam & traj_param) +{ + if (path_points.empty()) { + return false; + } + if (!isAvoidingObjectType(object, traj_param)) { + return false; + } + const auto image_point = geometry_utils::transformMapToOptionalImage( + object.kinematics.initial_pose_with_covariance.pose.position, map_info); + if (!image_point) { + return false; + } + + const int nearest_idx = tier4_autoware_utils::findNearestIndex( + path_points, object.kinematics.initial_pose_with_covariance.pose.position); + const auto nearest_path_point = path_points[nearest_idx]; + const auto rel_p = geometry_utils::transformToRelativeCoordinate2D( + object.kinematics.initial_pose_with_covariance.pose.position, nearest_path_point.pose); + // skip object located back the beginning of path points + if (nearest_idx == 0 && rel_p.x < 0) { + return false; + } + + /* + const float object_clearance_from_road = + clearance_map.ptr( + static_cast(image_point.get().y))[static_cast(image_point.get().x)] * + map_info.resolution; + */ + const geometry_msgs::msg::Vector3 twist = + object.kinematics.initial_twist_with_covariance.twist.linear; + const double vel = std::sqrt(twist.x * twist.x + twist.y * twist.y + twist.z * twist.z); + /* + const auto nearest_path_point_image = + geometry_utils::transformMapToOptionalImage(nearest_path_point.pose.position, map_info); + if (!nearest_path_point_image) { + return false; + } + const float nearest_path_point_clearance = + clearance_map.ptr(static_cast( + nearest_path_point_image.get().y))[static_cast(nearest_path_point_image.get().x)] * + map_info.resolution; + */ + const double lateral_offset_to_path = tier4_autoware_utils::calcLateralOffset( + path_points, object.kinematics.initial_pose_with_covariance.pose.position); + if ( + // nearest_path_point_clearance - traj_param.center_line_width * 0.5 < + // object_clearance_from_road || + std::abs(lateral_offset_to_path) < traj_param.center_line_width * 0.5 || + vel > traj_param.max_avoiding_objects_velocity_ms || + !arePointsInsideDriveableArea(polygon_points.points_in_image, clearance_map)) { + return false; + } + return true; +} +} // namespace + +namespace tier4_autoware_utils +{ +template <> +geometry_msgs::msg::Point getPoint(const cv::Point & p) +{ + geometry_msgs::msg::Point geom_p; + geom_p.x = p.x; + geom_p.y = p.y; + + return geom_p; +} +} // namespace tier4_autoware_utils + +CVMaps CostmapGenerator::getMaps( + const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & path, + const std::vector & objects, + const TrajectoryParam & traj_param, std::shared_ptr debug_data_ptr) +{ + stop_watch_.tic(__func__); + + // make cv_maps + CVMaps cv_maps; + + cv_maps.drivable_area = getDrivableAreaInCV(path.drivable_area, debug_data_ptr); + cv_maps.clearance_map = getClearanceMap(cv_maps.drivable_area, debug_data_ptr); + + std::vector debug_avoiding_objects; + cv::Mat objects_image = drawObstaclesOnImage( + enable_avoidance, objects, path.points, path.drivable_area.info, cv_maps.drivable_area, + cv_maps.clearance_map, traj_param, &debug_avoiding_objects, debug_data_ptr); + + cv_maps.area_with_objects_map = + getAreaWithObjects(cv_maps.drivable_area, objects_image, debug_data_ptr); + cv_maps.only_objects_clearance_map = getClearanceMap(objects_image, debug_data_ptr); + cv_maps.map_info = path.drivable_area.info; + + // debug data + debug_data_ptr->clearance_map = cv_maps.clearance_map; + debug_data_ptr->only_object_clearance_map = cv_maps.only_objects_clearance_map; + debug_data_ptr->area_with_objects_map = cv_maps.area_with_objects_map; + debug_data_ptr->avoiding_objects = debug_avoiding_objects; + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return cv_maps; +} + +cv::Mat CostmapGenerator::getDrivableAreaInCV( + const nav_msgs::msg::OccupancyGrid & occupancy_grid, + std::shared_ptr debug_data_ptr) const +{ + stop_watch_.tic(__func__); + + cv::Mat drivable_area = cv::Mat(occupancy_grid.info.width, occupancy_grid.info.height, CV_8UC1); + + drivable_area.forEach([&](unsigned char & value, const int * position) -> void { + cv_utils::getOccupancyGridValue(occupancy_grid, position[0], position[1], value); + }); + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return drivable_area; +} + +cv::Mat CostmapGenerator::getClearanceMap( + const cv::Mat & drivable_area, std::shared_ptr debug_data_ptr) const +{ + stop_watch_.tic(__func__); + + cv::Mat clearance_map; + cv::distanceTransform(drivable_area, clearance_map, cv::DIST_L2, 5); + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return clearance_map; +} + +cv::Mat CostmapGenerator::drawObstaclesOnImage( + const bool enable_avoidance, + const std::vector & objects, + const std::vector & path_points, + const nav_msgs::msg::MapMetaData & map_info, [[maybe_unused]] const cv::Mat & drivable_area, + const cv::Mat & clearance_map, const TrajectoryParam & traj_param, + std::vector * debug_avoiding_objects, + std::shared_ptr debug_data_ptr) const +{ + stop_watch_.tic(__func__); + + std::vector path_points_inside_area; + for (const auto & point : path_points) { + std::vector points; + geometry_msgs::msg::Point image_point; + if (!geometry_utils::transformMapToImage(point.pose.position, map_info, image_point)) { + continue; + } + const float clearance = + clearance_map.ptr(static_cast(image_point.y))[static_cast(image_point.x)]; + if (clearance < 1e-5) { + continue; + } + path_points_inside_area.push_back(point); + } + + // NOTE: objects image is too sparse so that creating cost map is heavy. + // Then, objects image is created by filling dilated drivable area, + // instead of "cv::Mat objects_image = cv::Mat::ones(clearance_map.rows, clearance_map.cols, + // CV_8UC1) * 255;". + constexpr double dilate_margin = 1.0; + cv::Mat objects_image; + const int dilate_size = static_cast( + (1.8 + dilate_margin) / + (std::sqrt(2) * map_info.resolution)); // TODO(murooka) use clearance_from_object + cv::dilate(drivable_area, objects_image, cv::Mat(), cv::Point(-1, -1), dilate_size); + + if (!enable_avoidance) { + return objects_image; + } + + // fill object + std::vector> cv_polygons; + std::vector> obj_cog_info; + std::vector obj_positions; + for (const auto & object : objects) { + const PolygonPoints polygon_points = cv_polygon_utils::getPolygonPoints(object, map_info); + if (isAvoidingObject( + polygon_points, object, clearance_map, map_info, path_points_inside_area, traj_param)) { + const double lon_dist_to_path = tier4_autoware_utils::calcSignedArcLength( + path_points, 0, object.kinematics.initial_pose_with_covariance.pose.position); + const double lat_dist_to_path = tier4_autoware_utils::calcLateralOffset( + path_points, object.kinematics.initial_pose_with_covariance.pose.position); + obj_cog_info.push_back({lon_dist_to_path, lat_dist_to_path}); + obj_positions.push_back(object.kinematics.initial_pose_with_covariance.pose.position); + + cv_polygons.push_back(cv_polygon_utils::getCVPolygon( + object, polygon_points, path_points_inside_area, clearance_map, map_info)); + debug_avoiding_objects->push_back(object); + } + } + for (const auto & polygon : cv_polygons) { + cv::fillConvexPoly(objects_image, polygon, cv::Scalar(0)); + } + + // fill between objects in the same side + const auto get_closest_obj_point = [&](size_t idx) { + const auto & path_point = + path_points.at(tier4_autoware_utils::findNearestIndex(path_points, obj_positions.at(idx))); + double min_dist = std::numeric_limits::min(); + size_t min_idx = 0; + for (size_t p_idx = 0; p_idx < cv_polygons.at(idx).size(); ++p_idx) { + const double dist = + tier4_autoware_utils::calcDistance2d(cv_polygons.at(idx).at(p_idx), path_point); + if (dist < min_dist) { + min_dist = dist; + min_idx = p_idx; + } + } + + geometry_msgs::msg::Point geom_point; + geom_point.x = cv_polygons.at(idx).at(min_idx).x; + geom_point.y = cv_polygons.at(idx).at(min_idx).y; + return geom_point; + }; + + std::vector> cv_between_polygons; + for (size_t i = 0; i < obj_positions.size(); ++i) { + for (size_t j = i + 1; j < obj_positions.size(); ++j) { + const auto & obj_info1 = obj_cog_info.at(i); + const auto & obj_info2 = obj_cog_info.at(j); + + // RCLCPP_ERROR_STREAM(rclcpp::get_logger("lat"), obj_info1.at(1) << " " << obj_info2.at(1)); + // RCLCPP_ERROR_STREAM(rclcpp::get_logger("lon"), obj_info1.at(0) << " " << obj_info2.at(0)); + + constexpr double max_lon_dist_to_convex_obstacles = 30.0; + if ( + obj_info1.at(1) * obj_info2.at(1) < 0 || + std::abs(obj_info1.at(0) - obj_info2.at(0)) > max_lon_dist_to_convex_obstacles) { + continue; + } + + std::vector cv_points; + cv_points.push_back(toCVPoint(obj_positions.at(i))); + cv_points.push_back(toCVPoint(get_closest_obj_point(i))); + cv_points.push_back(toCVPoint(get_closest_obj_point(j))); + cv_points.push_back(toCVPoint(obj_positions.at(j))); + + cv_between_polygons.push_back(cv_points); + } + } + /* + for (const auto & polygon : cv_between_polygons) { + cv::fillConvexPoly(objects_image, polygon, cv::Scalar(0)); + } + */ + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + + return objects_image; +} + +cv::Mat CostmapGenerator::getAreaWithObjects( + const cv::Mat & drivable_area, const cv::Mat & objects_image, + std::shared_ptr debug_data_ptr) const +{ + stop_watch_.tic(__func__); + + cv::Mat area_with_objects = cv::min(objects_image, drivable_area); + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return area_with_objects; +} diff --git a/planning/obstacle_avoidance_planner/src/cv_utils.cpp b/planning/obstacle_avoidance_planner/src/cv_utils.cpp new file mode 100644 index 0000000000000..a73a5c70fdeb3 --- /dev/null +++ b/planning/obstacle_avoidance_planner/src/cv_utils.cpp @@ -0,0 +1,463 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 "obstacle_avoidance_planner/cv_utils.hpp" + +#include "obstacle_avoidance_planner/utils.hpp" +#include "tf2/utils.h" +#include "tier4_autoware_utils/system/stop_watch.hpp" + +#include "autoware_auto_perception_msgs/msg/predicted_object.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "boost/optional.hpp" + +#include +#include +#include + +namespace +{ +boost::optional getDistance( + const cv::Mat & clearance_map, const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & map_info) +{ + const auto image_point = geometry_utils::transformMapToOptionalImage(map_point, map_info); + if (!image_point) { + return boost::none; + } + const float clearance = clearance_map.ptr(static_cast( + image_point.get().y))[static_cast(image_point.get().x)] * + map_info.resolution; + return clearance; +} + +bool isOutsideDrivableArea( + const geometry_msgs::msg::Point & pos, const cv::Mat & road_clearance_map, + const nav_msgs::msg::MapMetaData & map_info, const double max_dist) +{ + const auto dist = getDistance(road_clearance_map, pos, map_info); + if (dist) { + return dist.get() < max_dist; + } + + return false; +} +} // namespace + +namespace cv_utils +{ +void getOccupancyGridValue( + const nav_msgs::msg::OccupancyGrid & og, const int i, const int j, unsigned char & value) +{ + int i_flip = og.info.width - i - 1; + int j_flip = og.info.height - j - 1; + if (og.data[i_flip + j_flip * og.info.width] > 0) { + value = 0; + } else { + value = 255; + } +} + +void putOccupancyGridValue( + nav_msgs::msg::OccupancyGrid & og, const int i, const int j, const unsigned char value) +{ + int i_flip = og.info.width - i - 1; + int j_flip = og.info.height - j - 1; + og.data[i_flip + j_flip * og.info.width] = value; +} +} // namespace cv_utils + +namespace cv_polygon_utils +{ +PolygonPoints getPolygonPoints( + const std::vector & points, + const nav_msgs::msg::MapMetaData & map_info) +{ + std::vector points_in_image; + std::vector points_in_map; + for (const auto & point : points) { + const auto image_point = geometry_utils::transformMapToOptionalImage(point, map_info); + if (image_point) { + points_in_image.push_back(image_point.get()); + points_in_map.push_back(point); + } + } + PolygonPoints polygon_points; + polygon_points.points_in_image = points_in_image; + polygon_points.points_in_map = points_in_map; + return polygon_points; +} + +PolygonPoints getPolygonPoints( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info) +{ + std::vector points_in_image; + std::vector points_in_map; + PolygonPoints polygon_points; + if (object.shape.type == object.shape.BOUNDING_BOX) { + polygon_points = getPolygonPointsFromBB(object, map_info); + } else if (object.shape.type == object.shape.CYLINDER) { + polygon_points = getPolygonPointsFromCircle(object, map_info); + } else if (object.shape.type == object.shape.POLYGON) { + polygon_points = getPolygonPointsFromPolygon(object, map_info); + } + return polygon_points; +} + +PolygonPoints getPolygonPointsFromBB( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info) +{ + std::vector points_in_image; + std::vector points_in_map; + const double dim_x = object.shape.dimensions.x; + const double dim_y = object.shape.dimensions.y; + const std::vector rel_x = {0.5 * dim_x, 0.5 * dim_x, -0.5 * dim_x, -0.5 * dim_x}; + const std::vector rel_y = {0.5 * dim_y, -0.5 * dim_y, -0.5 * dim_y, 0.5 * dim_y}; + const geometry_msgs::msg::Pose object_pose = object.kinematics.initial_pose_with_covariance.pose; + for (size_t i = 0; i < rel_x.size(); i++) { + geometry_msgs::msg::Point rel_point; + rel_point.x = rel_x[i]; + rel_point.y = rel_y[i]; + auto abs_point = geometry_utils::transformToAbsoluteCoordinate2D(rel_point, object_pose); + geometry_msgs::msg::Point image_point; + if (geometry_utils::transformMapToImage(abs_point, map_info, image_point)) { + points_in_image.push_back(image_point); + points_in_map.push_back(abs_point); + } + } + PolygonPoints polygon_points; + polygon_points.points_in_image = points_in_image; + polygon_points.points_in_map = points_in_map; + return polygon_points; +} + +PolygonPoints getPolygonPointsFromCircle( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info) +{ + std::vector points_in_image; + std::vector points_in_map; + const double radius = object.shape.dimensions.x; + const geometry_msgs::msg::Point center = + object.kinematics.initial_pose_with_covariance.pose.position; + constexpr int num_sampling_points = 5; + for (int i = 0; i < num_sampling_points; ++i) { + std::vector deltas = {0, 1.0}; + for (const auto & delta : deltas) { + geometry_msgs::msg::Point point; + point.x = std::cos( + ((i + delta) / static_cast(num_sampling_points)) * 2.0 * M_PI + + M_PI / static_cast(num_sampling_points)) * + (radius / 2.0) + + center.x; + point.y = std::sin( + ((i + delta) / static_cast(num_sampling_points)) * 2.0 * M_PI + + M_PI / static_cast(num_sampling_points)) * + (radius / 2.0) + + center.y; + point.z = center.z; + geometry_msgs::msg::Point image_point; + if (geometry_utils::transformMapToImage(point, map_info, image_point)) { + points_in_image.push_back(image_point); + points_in_map.push_back(point); + } + } + } + PolygonPoints polygon_points; + polygon_points.points_in_image = points_in_image; + polygon_points.points_in_map = points_in_map; + return polygon_points; +} + +PolygonPoints getPolygonPointsFromPolygon( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const nav_msgs::msg::MapMetaData & map_info) +{ + std::vector points_in_image; + std::vector points_in_map; + for (const auto & polygon_p : object.shape.footprint.points) { + geometry_msgs::msg::Point rel_point; + rel_point.x = polygon_p.x; + rel_point.y = polygon_p.y; + geometry_msgs::msg::Point point = geometry_utils::transformToAbsoluteCoordinate2D( + rel_point, object.kinematics.initial_pose_with_covariance.pose); + const auto image_point = geometry_utils::transformMapToOptionalImage(point, map_info); + if (image_point) { + points_in_image.push_back(image_point.get()); + points_in_map.push_back(point); + } + } + PolygonPoints polygon_points; + polygon_points.points_in_image = points_in_image; + polygon_points.points_in_map = points_in_map; + return polygon_points; +} + +std::vector getCVPolygon( + const autoware_auto_perception_msgs::msg::PredictedObject & object, + const PolygonPoints & polygon_points, + const std::vector & path_points, + const cv::Mat & clearance_map, const nav_msgs::msg::MapMetaData & map_info) +{ + const int nearest_idx = tier4_autoware_utils::findNearestIndex( + path_points, object.kinematics.initial_pose_with_covariance.pose.position); + const auto nearest_path_point = path_points[nearest_idx]; + if (path_points.empty()) { + return getDefaultCVPolygon(polygon_points.points_in_image); + } + return getExtendedCVPolygon( + polygon_points.points_in_image, polygon_points.points_in_map, nearest_path_point.pose, object, + clearance_map, map_info); +} + +std::vector getDefaultCVPolygon( + const std::vector & points_in_image) +{ + std::vector cv_polygon; + for (const auto & point : points_in_image) { + cv::Point image_point = cv::Point(point.x, point.y); + cv_polygon.push_back(image_point); + } + return cv_polygon; +} + +std::vector getExtendedCVPolygon( + const std::vector & points_in_image, + const std::vector & points_in_map, + const geometry_msgs::msg::Pose & nearest_path_point_pose, + const autoware_auto_perception_msgs::msg::PredictedObject & object, const cv::Mat & clearance_map, + const nav_msgs::msg::MapMetaData & map_info) +{ + const boost::optional optional_edges = getEdges( + points_in_image, points_in_map, nearest_path_point_pose, object, clearance_map, map_info); + if (!optional_edges) { + return getDefaultCVPolygon(points_in_image); + } + const Edges edges = optional_edges.get(); + + const int nearest_polygon_idx = + tier4_autoware_utils::findNearestIndex(points_in_image, edges.origin); + std::vector cv_polygon; + if (edges.back_idx == nearest_polygon_idx || edges.front_idx == nearest_polygon_idx) { + // make polygon only with edges and extended edges + } else if (edges.back_idx < nearest_polygon_idx) { + // back_idx -> nearest_idx -> frond_idx + if (edges.back_idx < edges.front_idx && nearest_polygon_idx < edges.front_idx) { + for (int i = edges.back_idx + 1; i < edges.front_idx; i++) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + // back_idx -> vector_front -> vector_back -> nearest_idx -> frond_idx + } else if (edges.back_idx < edges.front_idx && nearest_polygon_idx > edges.front_idx) { + for (int i = edges.back_idx - 1; i >= 0; i--) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + for (int i = points_in_image.size() - 1; i > edges.front_idx; i--) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + // back_idx -> vector_back -> vector_front -> nearest_idx -> front_idx + } else { + for (size_t i = edges.back_idx + 1; i < points_in_image.size(); i++) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + for (int i = 0; i < edges.front_idx; i++) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + } + } else { + // back_idx -> nearest_idx -> front_idx + if (edges.back_idx >= edges.front_idx && nearest_polygon_idx > edges.front_idx) { + for (int i = edges.back_idx - 1; i > edges.front_idx; i--) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + // back_idx -> vector_back -> vector_front -> nearest_idx -> front_idx + } else { + if (edges.back_idx >= edges.front_idx && nearest_polygon_idx < edges.front_idx) { + for (size_t i = edges.back_idx + 1; i < points_in_image.size(); i++) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + for (int i = 0; i < edges.front_idx; i++) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + } else { // back_idx -> vector_front -> vector_back -> nearest_idx -> front_idx + for (int i = edges.back_idx - 1; i >= 0; i--) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + for (int i = points_in_image.size() - 1; i > edges.front_idx; i--) { + cv_polygon.push_back(cv::Point(points_in_image[i].x, points_in_image[i].y)); + } + } + } + } + cv_polygon.push_back( + cv::Point(points_in_image[edges.front_idx].x, points_in_image[edges.front_idx].y)); + cv_polygon.push_back(cv::Point(edges.extended_front.x, edges.extended_front.y)); + cv_polygon.push_back(cv::Point(edges.extended_back.x, edges.extended_back.y)); + cv_polygon.push_back( + cv::Point(points_in_image[edges.back_idx].x, points_in_image[edges.back_idx].y)); + return cv_polygon; +} + +boost::optional getEdges( + const std::vector & points_in_image, + const std::vector & points_in_map, + const geometry_msgs::msg::Pose & nearest_path_point_pose, + const autoware_auto_perception_msgs::msg::PredictedObject & object, const cv::Mat & clearance_map, + const nav_msgs::msg::MapMetaData & map_info) +{ + // calculate perpendicular point to object along with path point orientation + const double yaw = tf2::getYaw(nearest_path_point_pose.orientation); + const Eigen::Vector2d rel_path_vec(std::cos(yaw), std::sin(yaw)); + const Eigen::Vector2d obj_vec( + object.kinematics.initial_pose_with_covariance.pose.position.x, + object.kinematics.initial_pose_with_covariance.pose.position.y); + const double inner_product = rel_path_vec[0] * (obj_vec[0] - nearest_path_point_pose.position.x) + + rel_path_vec[1] * (obj_vec[1] - nearest_path_point_pose.position.y); + geometry_msgs::msg::Point origin; + origin.x = nearest_path_point_pose.position.x + rel_path_vec[0] * inner_product; + origin.y = nearest_path_point_pose.position.y + rel_path_vec[1] * inner_product; + const Eigen::Vector2d obj2origin(origin.x - obj_vec[0], origin.y - obj_vec[1]); + + // calculate origin for casting ray to edges + const auto path_point_image = + geometry_utils::transformMapToImage(nearest_path_point_pose.position, map_info); + constexpr double ray_origin_dist_scale = 1.0; + const float clearance = clearance_map.ptr(static_cast( + path_point_image.y))[static_cast(path_point_image.x)] * + map_info.resolution * ray_origin_dist_scale; + const Eigen::Vector2d obj2ray_origin = obj2origin.normalized() * (obj2origin.norm() + clearance); + geometry_msgs::msg::Point ray_origin; + ray_origin.x = obj_vec[0] + obj2ray_origin[0]; + ray_origin.y = obj_vec[1] + obj2ray_origin[1]; + geometry_msgs::msg::Point ray_origin_image; + ray_origin_image = geometry_utils::transformMapToImage(ray_origin, map_info); + + double min_cos = std::numeric_limits::max(); + double max_cos = std::numeric_limits::lowest(); + const double path_yaw = tf2::getYaw(nearest_path_point_pose.orientation); + const double dx1 = std::cos(path_yaw); + const double dy1 = std::sin(path_yaw); + const Eigen::Vector2d path_point_vec(dx1, dy1); + const double path_point_vec_norm = path_point_vec.norm(); + Edges edges; + for (size_t i = 0; i < points_in_image.size(); i++) { + const double dx2 = points_in_map[i].x - ray_origin.x; + const double dy2 = points_in_map[i].y - ray_origin.y; + const Eigen::Vector2d path_point2point(dx2, dy2); + const double inner_product = path_point_vec.dot(path_point2point); + const double cos = inner_product / (path_point_vec_norm * path_point2point.norm()); + if (cos > max_cos) { + max_cos = cos; + edges.front_idx = i; + } + if (cos < min_cos) { + min_cos = cos; + edges.back_idx = i; + } + } + + const double max_sin = std::sqrt(1 - max_cos * max_cos); + const double min_sin = std::sqrt(1 - min_cos * min_cos); + const Eigen::Vector2d point2front_edge( + points_in_image[edges.front_idx].x - ray_origin_image.x, + points_in_image[edges.front_idx].y - ray_origin_image.y); + const Eigen::Vector2d point2back_edge( + points_in_image[edges.back_idx].x - ray_origin_image.x, + points_in_image[edges.back_idx].y - ray_origin_image.y); + const Eigen::Vector2d point2extended_front_edge = + point2front_edge.normalized() * (clearance * 2 / max_sin) * (1 / map_info.resolution); + const Eigen::Vector2d point2extended_back_edge = + point2back_edge.normalized() * (clearance * 2 / min_sin) * (1 / map_info.resolution); + + const double dist2extended_front_edge = point2extended_front_edge.norm() * map_info.resolution; + const double dist2front_edge = point2front_edge.norm() * map_info.resolution; + const double dist2extended_back_edge = point2extended_back_edge.norm() * map_info.resolution; + const double dist2back_edge = point2back_edge.norm() * map_info.resolution; + if ( + dist2extended_front_edge < clearance * 2 || dist2extended_back_edge < clearance * 2 || + dist2front_edge > dist2extended_front_edge || dist2back_edge > dist2extended_back_edge) { + return boost::none; + } + geometry_msgs::msg::Point extended_front; + geometry_msgs::msg::Point extended_back; + extended_front.x = point2extended_front_edge(0) + ray_origin_image.x; + extended_front.y = point2extended_front_edge(1) + ray_origin_image.y; + extended_back.x = point2extended_back_edge(0) + ray_origin_image.x; + extended_back.y = point2extended_back_edge(1) + ray_origin_image.y; + edges.extended_front = extended_front; + edges.extended_back = extended_back; + edges.origin = ray_origin_image; + return edges; +} +} // namespace cv_polygon_utils + +namespace cv_drivable_area_utils +{ +bool isOutsideDrivableAreaFromRectangleFootprint( + const autoware_auto_planning_msgs::msg::TrajectoryPoint & traj_point, + const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, + const VehicleParam & vehicle_param) +{ + const double half_width = vehicle_param.width / 2.0; + const double base_to_front = vehicle_param.length - vehicle_param.rear_overhang; + const double base_to_rear = vehicle_param.rear_overhang; + + const auto top_left_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, base_to_front, -half_width, 0.0).position; + const auto top_right_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, base_to_front, half_width, 0.0).position; + const auto bottom_right_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, -base_to_rear, half_width, 0.0).position; + const auto bottom_left_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, -base_to_rear, -half_width, 0.0).position; + + constexpr double epsilon = 1e-8; + const bool out_top_left = + isOutsideDrivableArea(top_left_pos, road_clearance_map, map_info, epsilon); + const bool out_top_right = + isOutsideDrivableArea(top_right_pos, road_clearance_map, map_info, epsilon); + const bool out_bottom_left = + isOutsideDrivableArea(bottom_left_pos, road_clearance_map, map_info, epsilon); + const bool out_bottom_right = + isOutsideDrivableArea(bottom_right_pos, road_clearance_map, map_info, epsilon); + + if (out_top_left || out_top_right || out_bottom_left || out_bottom_right) { + return true; + } + + return false; +} + +bool isOutsideDrivableAreaFromCirclesFootprint( + const autoware_auto_planning_msgs::msg::TrajectoryPoint & traj_point, + const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, + const std::vector vehicle_circle_longitudinal_offsets, const double vehicle_circle_radius) +{ + for (const double offset : vehicle_circle_longitudinal_offsets) { + const auto avoiding_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, offset, 0.0, 0.0).position; + + const bool outside_drivable_area = + isOutsideDrivableArea(avoiding_pos, road_clearance_map, map_info, vehicle_circle_radius); + if (outside_drivable_area) { + return true; + } + } + + return false; +} + +} // namespace cv_drivable_area_utils diff --git a/planning/obstacle_avoidance_planner/src/debug.cpp b/planning/obstacle_avoidance_planner/src/debug.cpp index 7c0036f0b03ea..5b591c6412224 100644 --- a/planning/obstacle_avoidance_planner/src/debug.cpp +++ b/planning/obstacle_avoidance_planner/src/debug.cpp @@ -18,7 +18,6 @@ #include "obstacle_avoidance_planner/marker_helper.hpp" #include "obstacle_avoidance_planner/mpt_optimizer.hpp" #include "obstacle_avoidance_planner/process_cv.hpp" -#include "obstacle_avoidance_planner/util.hpp" #include #include diff --git a/planning/obstacle_avoidance_planner/src/debug_visualization.cpp b/planning/obstacle_avoidance_planner/src/debug_visualization.cpp new file mode 100644 index 0000000000000..86b3cc43420c0 --- /dev/null +++ b/planning/obstacle_avoidance_planner/src/debug_visualization.cpp @@ -0,0 +1,826 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 "obstacle_avoidance_planner/debug_visualization.hpp" + +#include "obstacle_avoidance_planner/cv_utils.hpp" +#include "obstacle_avoidance_planner/mpt_optimizer.hpp" +#include "obstacle_avoidance_planner/utils.hpp" +#include "tf2/utils.h" + +#include "visualization_msgs/msg/marker.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include +#include + +using tier4_autoware_utils::appendMarkerArray; +using tier4_autoware_utils::createDefaultMarker; +using tier4_autoware_utils::createMarkerColor; +using tier4_autoware_utils::createMarkerScale; + +namespace +{ +template +visualization_msgs::msg::MarkerArray getPointsMarkerArray( + const std::vector & points, const std::string & ns, const double r, const double g, + const double b) +{ + if (points.empty()) { + return visualization_msgs::msg::MarkerArray{}; + } + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::LINE_LIST, + createMarkerScale(0.5, 0.5, 0.5), createMarkerColor(r, g, b, 0.99)); + marker.lifetime = rclcpp::Duration::from_seconds(1.0); + + for (const auto & point : points) { + marker.points.push_back(tier4_autoware_utils::getPoint(point)); + } + + visualization_msgs::msg::MarkerArray msg; + msg.markers.push_back(marker); + + return msg; +} + +template +visualization_msgs::msg::MarkerArray getPointsTextMarkerArray( + const std::vector & points, const std::string & ns, const double r, const double g, + const double b) +{ + if (points.empty()) { + return visualization_msgs::msg::MarkerArray{}; + } + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::TEXT_VIEW_FACING, + createMarkerScale(0.0, 0.0, 0.15), createMarkerColor(r, g, b, 0.99)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + visualization_msgs::msg::MarkerArray msg; + for (size_t i = 0; i < points.size(); i++) { + marker.id = i; + marker.text = std::to_string(i); + marker.pose.position = tier4_autoware_utils::getPoint(points[i]); + msg.markers.push_back(marker); + } + + return msg; +} + +geometry_msgs::msg::Pose getVirtualWallPose( + const geometry_msgs::msg::Pose & target_pose, const VehicleParam & vehicle_param) +{ + const double base_link2front = vehicle_param.wheelbase + vehicle_param.front_overhang; + tf2::Transform tf_base_link2front( + tf2::Quaternion(0.0, 0.0, 0.0, 1.0), tf2::Vector3(base_link2front, 0.0, 0.0)); + tf2::Transform tf_map2base_link; + tf2::fromMsg(target_pose, tf_map2base_link); + tf2::Transform tf_map2front = tf_map2base_link * tf_base_link2front; + geometry_msgs::msg::Pose virtual_wall_pose; + tf2::toMsg(tf_map2front, virtual_wall_pose); + return virtual_wall_pose; +} + +visualization_msgs::msg::MarkerArray getDebugPointsMarkers( + const std::vector & interpolated_points, + const std::vector & optimized_points, + const std::vector & straight_points, + const std::vector & fixed_points, + const std::vector & non_fixed_points) +{ + visualization_msgs::msg::MarkerArray marker_array; + int unique_id = 0; + + unique_id = 0; + visualization_msgs::msg::Marker interpolated_points_marker; + interpolated_points_marker.lifetime = rclcpp::Duration::from_seconds(0); + interpolated_points_marker.header.frame_id = "map"; + interpolated_points_marker.header.stamp = rclcpp::Time(0); + interpolated_points_marker.ns = std::string("interpolated_points_marker"); + interpolated_points_marker.action = visualization_msgs::msg::Marker::ADD; + interpolated_points_marker.pose.orientation.w = 1.0; + interpolated_points_marker.id = unique_id; + interpolated_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + interpolated_points_marker.scale = createMarkerScale(.3, .3, .3); + interpolated_points_marker.color = createMarkerColor(1.0f, 0, 0, 0.8); + unique_id++; + for (const auto & point : interpolated_points) { + interpolated_points_marker.points.push_back(point); + } + if (!interpolated_points_marker.points.empty()) { + marker_array.markers.push_back(interpolated_points_marker); + } + + unique_id = 0; + visualization_msgs::msg::Marker optimized_points_marker; + optimized_points_marker.lifetime = rclcpp::Duration::from_seconds(0); + optimized_points_marker.header.frame_id = "map"; + optimized_points_marker.header.stamp = rclcpp::Time(0); + optimized_points_marker.ns = std::string("optimized_points_marker"); + optimized_points_marker.action = visualization_msgs::msg::Marker::ADD; + optimized_points_marker.pose.orientation.w = 1.0; + optimized_points_marker.id = unique_id; + optimized_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + optimized_points_marker.scale = createMarkerScale(0.35, 0.35, 0.35); + optimized_points_marker.color = createMarkerColor(0, 1.0f, 0, 0.99); + unique_id++; + for (const auto & point : optimized_points) { + optimized_points_marker.points.push_back(point.pose.position); + } + if (!optimized_points_marker.points.empty()) { + marker_array.markers.push_back(optimized_points_marker); + } + + unique_id = 0; + for (size_t i = 0; i < optimized_points.size(); i++) { + visualization_msgs::msg::Marker optimized_points_text_marker; + optimized_points_text_marker.lifetime = rclcpp::Duration::from_seconds(0); + optimized_points_text_marker.header.frame_id = "map"; + optimized_points_text_marker.header.stamp = rclcpp::Time(0); + optimized_points_text_marker.ns = std::string("optimized_points_text_marker"); + optimized_points_text_marker.action = visualization_msgs::msg::Marker::ADD; + optimized_points_text_marker.pose.orientation.w = 1.0; + optimized_points_text_marker.id = unique_id; + optimized_points_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + optimized_points_text_marker.pose.position = optimized_points[i].pose.position; + optimized_points_text_marker.scale = createMarkerScale(0, 0, 0.15); + optimized_points_text_marker.color = createMarkerColor(0, 1.0, 0, 0.99); + optimized_points_text_marker.text = std::to_string(i); + unique_id++; + marker_array.markers.push_back(optimized_points_text_marker); + } + + unique_id = 0; + for (size_t i = 0; i < interpolated_points.size(); i++) { + visualization_msgs::msg::Marker interpolated_points_text_marker; + interpolated_points_text_marker.lifetime = rclcpp::Duration::from_seconds(0); + interpolated_points_text_marker.header.frame_id = "map"; + interpolated_points_text_marker.header.stamp = rclcpp::Time(0); + interpolated_points_text_marker.ns = std::string("interpolated_points_text_marker"); + interpolated_points_text_marker.action = visualization_msgs::msg::Marker::ADD; + interpolated_points_text_marker.pose.orientation.w = 1.0; + interpolated_points_text_marker.id = unique_id; + interpolated_points_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + interpolated_points_text_marker.pose.position = interpolated_points[i]; + interpolated_points_text_marker.scale = createMarkerScale(0, 0, 0.5); + interpolated_points_text_marker.color = createMarkerColor(0, 1.0, 0, 0.99); + interpolated_points_text_marker.text = std::to_string(i); + unique_id++; + marker_array.markers.push_back(interpolated_points_text_marker); + } + + unique_id = 0; + visualization_msgs::msg::Marker straight_points_marker; + straight_points_marker.lifetime = rclcpp::Duration::from_seconds(40); + straight_points_marker.header.frame_id = "map"; + straight_points_marker.header.stamp = rclcpp::Time(0); + straight_points_marker.ns = std::string("straight_points_marker"); + straight_points_marker.action = visualization_msgs::msg::Marker::ADD; + straight_points_marker.pose.orientation.w = 1.0; + straight_points_marker.id = unique_id; + straight_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + straight_points_marker.scale = createMarkerScale(1.0, 1.0, 1.0); + straight_points_marker.color = createMarkerColor(1.0, 1.0, 0, 0.99); + unique_id++; + for (const auto & point : straight_points) { + straight_points_marker.points.push_back(point); + } + if (!straight_points_marker.points.empty()) { + marker_array.markers.push_back(straight_points_marker); + } + + unique_id = 0; + visualization_msgs::msg::Marker fixed_marker; + fixed_marker.lifetime = rclcpp::Duration::from_seconds(0); + fixed_marker.header.frame_id = "map"; + fixed_marker.header.stamp = rclcpp::Time(0); + fixed_marker.ns = std::string("fixed_points_marker"); + fixed_marker.action = visualization_msgs::msg::Marker::ADD; + fixed_marker.pose.orientation.w = 1.0; + fixed_marker.id = unique_id; + fixed_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + fixed_marker.scale = createMarkerScale(0.3, 0.3, 0.3); + fixed_marker.color = createMarkerColor(1.0, 0, 0, 0.99); + unique_id++; + for (const auto & point : fixed_points) { + fixed_marker.points.push_back(point.position); + } + if (!fixed_marker.points.empty()) { + marker_array.markers.push_back(fixed_marker); + } + + visualization_msgs::msg::Marker non_fixed_marker; + non_fixed_marker.lifetime = rclcpp::Duration::from_seconds(20); + non_fixed_marker.header.frame_id = "map"; + non_fixed_marker.header.stamp = rclcpp::Time(0); + non_fixed_marker.ns = std::string("non_fixed_points_marker"); + non_fixed_marker.action = visualization_msgs::msg::Marker::ADD; + non_fixed_marker.pose.orientation.w = 1.0; + non_fixed_marker.id = unique_id; + non_fixed_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + non_fixed_marker.scale = createMarkerScale(1.0, 1.0, 1.0); + non_fixed_marker.color = createMarkerColor(0, 1.0, 0, 0.99); + unique_id++; + for (const auto & point : non_fixed_points) { + non_fixed_marker.points.push_back(point.position); + } + if (!non_fixed_marker.points.empty()) { + marker_array.markers.push_back(non_fixed_marker); + } + return marker_array; +} + +visualization_msgs::msg::MarkerArray getDebugConstrainMarkers( + const std::vector & constrain_ranges, const std::string & ns) +{ + visualization_msgs::msg::MarkerArray marker_array; + int unique_id = 0; + for (size_t i = 0; i < constrain_ranges.size(); i++) { + visualization_msgs::msg::Marker constrain_rect_marker; + constrain_rect_marker.lifetime = rclcpp::Duration::from_seconds(0); + constrain_rect_marker.header.frame_id = "map"; + constrain_rect_marker.header.stamp = rclcpp::Time(0); + constrain_rect_marker.ns = ns; + constrain_rect_marker.action = visualization_msgs::msg::Marker::ADD; + constrain_rect_marker.pose.orientation.w = 1.0; + constrain_rect_marker.id = unique_id; + constrain_rect_marker.type = visualization_msgs::msg::Marker::LINE_STRIP; + constrain_rect_marker.scale = createMarkerScale(0.01, 0, 0); + constrain_rect_marker.color = createMarkerColor(1.0, 0, 0, 0.99); + unique_id++; + geometry_msgs::msg::Point top_left_point = constrain_ranges[i].top_left; + geometry_msgs::msg::Point top_right_point = constrain_ranges[i].top_right; + geometry_msgs::msg::Point bottom_right_point = constrain_ranges[i].bottom_right; + geometry_msgs::msg::Point bottom_left_point = constrain_ranges[i].bottom_left; + constrain_rect_marker.points.push_back(top_left_point); + constrain_rect_marker.points.push_back(top_right_point); + constrain_rect_marker.points.push_back(bottom_right_point); + constrain_rect_marker.points.push_back(bottom_left_point); + constrain_rect_marker.points.push_back(top_left_point); + marker_array.markers.push_back(constrain_rect_marker); + } + + for (size_t i = 0; i < constrain_ranges.size(); i++) { + visualization_msgs::msg::Marker marker; + marker.header.frame_id = "map"; + marker.header.stamp = rclcpp::Time(0); + marker.ns = ns + "_text"; + marker.id = unique_id++; + marker.lifetime = rclcpp::Duration::from_seconds(0); + marker.action = visualization_msgs::msg::Marker::ADD; + marker.pose.orientation.w = 1.0; + marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + marker.scale = createMarkerScale(0, 0, 0.15); + marker.color = createMarkerColor(1.0, 0, 0, 0.99); + marker.text = std::to_string(i); + marker.pose.position = constrain_ranges[i].top_left; + marker_array.markers.push_back(marker); + } + + unique_id = 0; + for (size_t i = 0; i < constrain_ranges.size(); i++) { + visualization_msgs::msg::Marker constrain_range_text_marker; + constrain_range_text_marker.lifetime = rclcpp::Duration::from_seconds(0); + constrain_range_text_marker.header.frame_id = "map"; + constrain_range_text_marker.header.stamp = rclcpp::Time(0); + constrain_range_text_marker.ns = ns + "_location"; + constrain_range_text_marker.action = visualization_msgs::msg::Marker::ADD; + constrain_range_text_marker.pose.orientation.w = 1.0; + constrain_range_text_marker.id = unique_id; + constrain_range_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + constrain_range_text_marker.pose.position = constrain_ranges[i].top_left; + constrain_range_text_marker.scale = createMarkerScale(0, 0, 0.1); + constrain_range_text_marker.color = createMarkerColor(1.0, 0, 0, 0.99); + constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + + std::to_string(constrain_range_text_marker.pose.position.x) + + std::string("y ") + + std::to_string(constrain_range_text_marker.pose.position.y); + unique_id++; + marker_array.markers.push_back(constrain_range_text_marker); + + constrain_range_text_marker.id = unique_id; + constrain_range_text_marker.pose.position = constrain_ranges[i].top_right; + constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + + std::to_string(constrain_range_text_marker.pose.position.x) + + std::string("y ") + + std::to_string(constrain_range_text_marker.pose.position.y); + unique_id++; + marker_array.markers.push_back(constrain_range_text_marker); + + constrain_range_text_marker.id = unique_id; + constrain_range_text_marker.pose.position = constrain_ranges[i].bottom_left; + constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + + std::to_string(constrain_range_text_marker.pose.position.x) + + std::string("y ") + + std::to_string(constrain_range_text_marker.pose.position.y); + unique_id++; + marker_array.markers.push_back(constrain_range_text_marker); + + constrain_range_text_marker.id = unique_id; + constrain_range_text_marker.pose.position = constrain_ranges[i].bottom_right; + constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + + std::to_string(constrain_range_text_marker.pose.position.x) + + std::string("y ") + + std::to_string(constrain_range_text_marker.pose.position.y); + unique_id++; + marker_array.markers.push_back(constrain_range_text_marker); + } + return marker_array; +} + +visualization_msgs::msg::MarkerArray getObjectsMarkerArray( + const std::vector & objects, + const std::string & ns, const double r, const double g, const double b) +{ + visualization_msgs::msg::MarkerArray msg; + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::CUBE, + createMarkerScale(3.0, 1.0, 1.0), createMarkerColor(r, g, b, 0.8)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + for (size_t i = 0; i < objects.size(); ++i) { + const auto & object = objects.at(i); + marker.id = i; + marker.pose = object.kinematics.initial_pose_with_covariance.pose; + msg.markers.push_back(marker); + } + + return msg; +} + +visualization_msgs::msg::MarkerArray getRectanglesMarkerArray( + const std::vector mpt_traj, + const VehicleParam & vehicle_param, const std::string & ns, const double r, const double g, + const double b, const size_t sampling_num) +{ + visualization_msgs::msg::MarkerArray msg; + for (size_t i = 0; i < mpt_traj.size(); ++i) { + if (i % sampling_num != 0) { + continue; + } + const auto & traj_point = mpt_traj.at(i); + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, i, visualization_msgs::msg::Marker::LINE_STRIP, + createMarkerScale(0.05, 0.0, 0.0), createMarkerColor(r, g, b, 1.0)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + const double half_width = vehicle_param.width / 2.0; + const double base_to_front = vehicle_param.length - vehicle_param.rear_overhang; + const double base_to_rear = vehicle_param.rear_overhang; + + marker.points.push_back( + tier4_autoware_utils::calcOffsetPose(traj_point.pose, base_to_front, -half_width, 0.0) + .position); + marker.points.push_back( + tier4_autoware_utils::calcOffsetPose(traj_point.pose, base_to_front, half_width, 0.0) + .position); + marker.points.push_back( + tier4_autoware_utils::calcOffsetPose(traj_point.pose, -base_to_rear, half_width, 0.0) + .position); + marker.points.push_back( + tier4_autoware_utils::calcOffsetPose(traj_point.pose, -base_to_rear, -half_width, 0.0) + .position); + marker.points.push_back(marker.points.front()); + + msg.markers.push_back(marker); + } + return msg; +} + +visualization_msgs::msg::MarkerArray getRectanglesNumMarkerArray( + const std::vector mpt_traj, + const VehicleParam & vehicle_param, const std::string & ns, const double r, const double g, + const double b) +{ + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::TEXT_VIEW_FACING, + createMarkerScale(0.0, 0.0, 0.125), createMarkerColor(r, g, b, 0.99)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + visualization_msgs::msg::MarkerArray msg; + for (size_t i = 0; i < mpt_traj.size(); ++i) { + const auto & traj_point = mpt_traj.at(i); + + marker.text = std::to_string(i); + + const double half_width = vehicle_param.width / 2.0; + const double base_to_front = vehicle_param.length - vehicle_param.rear_overhang; + + const auto top_right_pos = + tier4_autoware_utils::calcOffsetPose(traj_point.pose, base_to_front, half_width, 0.0) + .position; + marker.id = i; + marker.pose.position = top_right_pos; + msg.markers.push_back(marker); + + marker.id = i + mpt_traj.size(); + marker.pose.position = top_right_pos; + msg.markers.push_back(marker); + } + return msg; +} + +/* +visualization_msgs::msg::MarkerArray getBoundsCandidatesLineMarkerArray( + const std::vector & ref_points, + const SequentialBoundsCandidates & sequential_bounds_candidates, const double r, const double g, + const double b, const double vehicle_circle_radius, const size_t sampling_num) +{ + const auto current_time = rclcpp::Clock().now(); + visualization_msgs::msg::MarkerArray msg; + + int unique_id = 0; + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), "bounds_candidates", 0, + visualization_msgs::msg::Marker::LINE_LIST, createMarkerScale(0.05, 0, 0), + createMarkerColor(r + 0.5, g, b, 0.3)); + marker.lifetime = rclcpp::Duration::from_seconds(1.0); + + for (size_t p_idx = 0; p_idx < sequential_bounds_candidates.size(); ++p_idx) { + if (p_idx % sampling_num != 0) { + continue; + } + + const auto & bounds_candidates = sequential_bounds_candidates.at(p_idx); + for (size_t b_idx = 0; b_idx < bounds_candidates.size(); ++b_idx) { + const auto & bounds_candidate = bounds_candidates.at(b_idx); + + marker.id = unique_id++; + geometry_msgs::msg::Pose pose; + pose.position = ref_points.at(p_idx).p; + pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(ref_points.at(p_idx).yaw); + + const double lb_y = bounds_candidate.lower_bound - vehicle_circle_radius; + const auto lb = tier4_autoware_utils::calcOffsetPose(pose, 0.0, lb_y, 0.0).position; + marker.points.push_back(lb); + + const double ub_y = bounds_candidate.upper_bound + vehicle_circle_radius; + const auto ub = tier4_autoware_utils::calcOffsetPose(pose, 0.0, ub_y, 0.0).position; + marker.points.push_back(ub); + } + } + + msg.markers.push_back(marker); + return msg; +} +*/ + +visualization_msgs::msg::MarkerArray getBoundsLineMarkerArray( + const std::vector & ref_points, const double r, const double g, const double b, + const double vehicle_circle_radius, const size_t sampling_num) +{ + const auto current_time = rclcpp::Clock().now(); + visualization_msgs::msg::MarkerArray msg; + + if (ref_points.empty()) return msg; + + for (size_t bound_idx = 0; bound_idx < ref_points.at(0).vehicle_bounds.size(); ++bound_idx) { + const std::string ns = "base_bounds_" + std::to_string(bound_idx); + + { // lower bound + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::LINE_LIST, + createMarkerScale(0.05, 0.0, 0.0), createMarkerColor(r + 0.5, g, b, 0.3)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + for (size_t i = 0; i < ref_points.size(); i++) { + if (i % sampling_num != 0) { + continue; + } + + const geometry_msgs::msg::Pose & pose = ref_points.at(i).vehicle_bounds_poses.at(bound_idx); + const double lb_y = + ref_points.at(i).vehicle_bounds[bound_idx].lower_bound - vehicle_circle_radius; + const auto lb = tier4_autoware_utils::calcOffsetPose(pose, 0.0, lb_y, 0.0).position; + + marker.points.push_back(pose.position); + marker.points.push_back(lb); + } + msg.markers.push_back(marker); + } + + { // upper bound + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 1, visualization_msgs::msg::Marker::LINE_LIST, + createMarkerScale(0.05, 0.0, 0.0), createMarkerColor(r, g + 0.5, b, 0.3)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + for (size_t i = 0; i < ref_points.size(); i++) { + if (i % sampling_num != 0) { + continue; + } + + const geometry_msgs::msg::Pose & pose = ref_points.at(i).vehicle_bounds_poses.at(bound_idx); + const double ub_y = + ref_points.at(i).vehicle_bounds[bound_idx].upper_bound + vehicle_circle_radius; + const auto ub = tier4_autoware_utils::calcOffsetPose(pose, 0.0, ub_y, 0.0).position; + + marker.points.push_back(pose.position); + marker.points.push_back(ub); + } + msg.markers.push_back(marker); + } + } + + return msg; +} + +visualization_msgs::msg::MarkerArray getVehicleCircleLineMarkerArray( + const std::vector> & vehicle_circles_pose, + const double vehicle_circle_radius, const size_t sampling_num, const std::string & ns, + const double r, const double g, const double b) +{ + const auto current_time = rclcpp::Clock().now(); + visualization_msgs::msg::MarkerArray msg; + + for (size_t i = 0; i < vehicle_circles_pose.size(); ++i) { + if (i % sampling_num != 0) { + continue; + } + + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, i, visualization_msgs::msg::Marker::LINE_LIST, + createMarkerScale(0.1, 0, 0), createMarkerColor(r, g, b, 0.25)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + for (size_t j = 0; j < vehicle_circles_pose.at(i).size(); ++j) { + const geometry_msgs::msg::Pose & pose = vehicle_circles_pose.at(i).at(j); + const auto ub = + tier4_autoware_utils::calcOffsetPose(pose, 0.0, vehicle_circle_radius, 0.0).position; + const auto lb = + tier4_autoware_utils::calcOffsetPose(pose, 0.0, -vehicle_circle_radius, 0.0).position; + + marker.points.push_back(ub); + marker.points.push_back(lb); + } + msg.markers.push_back(marker); + } + + return msg; +} + +visualization_msgs::msg::MarkerArray getLateralErrorsLineMarkerArray( + const std::vector mpt_ref_poses, std::vector lateral_errors, + const size_t sampling_num, const std::string & ns, const double r, const double g, const double b) +{ + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::LINE_LIST, + createMarkerScale(0.1, 0, 0), createMarkerColor(r, g, b, 1.0)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + + for (size_t i = 0; i < mpt_ref_poses.size(); ++i) { + if (i % sampling_num != 0) { + continue; + } + + const auto vehicle_pose = + tier4_autoware_utils::calcOffsetPose(mpt_ref_poses.at(i), 0.0, lateral_errors.at(i), 0.0); + marker.points.push_back(mpt_ref_poses.at(i).position); + marker.points.push_back(vehicle_pose.position); + } + + visualization_msgs::msg::MarkerArray msg; + msg.markers.push_back(marker); + + return msg; +} + +visualization_msgs::msg::MarkerArray getCurrentVehicleCirclesMarkerArray( + const geometry_msgs::msg::Pose & current_ego_pose, + const std::vector & vehicle_circle_longitudinal_offsets, + const double vehicle_circle_radius, const std::string & ns, const double r, const double g, + const double b) +{ + visualization_msgs::msg::MarkerArray msg; + + size_t id = 0; + for (const double offset : vehicle_circle_longitudinal_offsets) { + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, id, visualization_msgs::msg::Marker::LINE_STRIP, + createMarkerScale(0.05, 0.0, 0.0), createMarkerColor(r, g, b, 0.8)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + marker.pose = tier4_autoware_utils::calcOffsetPose(current_ego_pose, offset, 0.0, 0.0); + + constexpr size_t circle_dividing_num = 16; + for (size_t e_idx = 0; e_idx < circle_dividing_num + 1; ++e_idx) { + geometry_msgs::msg::Point edge_pos; + + const double edge_angle = + static_cast(e_idx) / static_cast(circle_dividing_num) * 2.0 * M_PI; + edge_pos.x = vehicle_circle_radius * std::cos(edge_angle); + edge_pos.y = vehicle_circle_radius * std::sin(edge_angle); + + marker.points.push_back(edge_pos); + } + + msg.markers.push_back(marker); + ++id; + } + return msg; +} + +visualization_msgs::msg::MarkerArray getVehicleCirclesMarkerArray( + const std::vector & mpt_traj_points, + const std::vector & vehicle_circle_longitudinal_offsets, + const double vehicle_circle_radius, const size_t sampling_num, const std::string & ns, + const double r, const double g, const double b) +{ + visualization_msgs::msg::MarkerArray msg; + + size_t id = 0; + for (size_t i = 0; i < mpt_traj_points.size(); ++i) { + if (i % sampling_num != 0) { + continue; + } + const auto & mpt_traj_point = mpt_traj_points.at(i); + + for (const double offset : vehicle_circle_longitudinal_offsets) { + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, id, visualization_msgs::msg::Marker::LINE_STRIP, + createMarkerScale(0.05, 0.0, 0.0), createMarkerColor(r, g, b, 0.8)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + marker.pose = tier4_autoware_utils::calcOffsetPose(mpt_traj_point.pose, offset, 0.0, 0.0); + + constexpr size_t circle_dividing_num = 16; + for (size_t e_idx = 0; e_idx < circle_dividing_num + 1; ++e_idx) { + geometry_msgs::msg::Point edge_pos; + + const double edge_angle = + static_cast(e_idx) / static_cast(circle_dividing_num) * 2.0 * M_PI; + edge_pos.x = vehicle_circle_radius * std::cos(edge_angle); + edge_pos.y = vehicle_circle_radius * std::sin(edge_angle); + + marker.points.push_back(edge_pos); + } + + msg.markers.push_back(marker); + ++id; + } + } + return msg; +} + +visualization_msgs::msg::MarkerArray getVirtualWallMarkerArray( + const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, + const double b) +{ + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::CUBE, + createMarkerScale(0.1, 5.0, 2.0), createMarkerColor(r, g, b, 0.5)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + marker.pose = pose; + + visualization_msgs::msg::MarkerArray msg; + msg.markers.push_back(marker); + + return msg; +} + +visualization_msgs::msg::MarkerArray getVirtualWallTextMarkerArray( + const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, + const double b) +{ + auto marker = createDefaultMarker( + "map", rclcpp::Clock().now(), ns, 0, visualization_msgs::msg::Marker::TEXT_VIEW_FACING, + createMarkerScale(0.0, 0.0, 1.0), createMarkerColor(r, g, b, 0.99)); + marker.lifetime = rclcpp::Duration::from_seconds(1.5); + marker.pose = pose; + marker.text = "drivable area"; + + visualization_msgs::msg::MarkerArray msg; + msg.markers.push_back(marker); + + return msg; +} +} // namespace + +namespace debug_visualization +{ +visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( + const std::shared_ptr debug_data_ptr, + const std::vector & optimized_points, + const VehicleParam & vehicle_param, const bool is_showing_debug_detail) +{ + // virtual wall + visualization_msgs::msg::MarkerArray vis_marker_array; + if (debug_data_ptr->stop_pose_by_drivable_area) { + const auto virtual_wall_pose = + getVirtualWallPose(debug_data_ptr->stop_pose_by_drivable_area.get(), vehicle_param); + appendMarkerArray( + getVirtualWallMarkerArray(virtual_wall_pose, "virtual_wall", 1.0, 0, 0), &vis_marker_array); + appendMarkerArray( + getVirtualWallTextMarkerArray(virtual_wall_pose, "virtual_wall_text", 1.0, 1.0, 1.0), + &vis_marker_array); + } + + if (is_showing_debug_detail) { + const auto points_marker_array = getDebugPointsMarkers( + debug_data_ptr->interpolated_points, optimized_points, debug_data_ptr->straight_points, + debug_data_ptr->fixed_points, debug_data_ptr->non_fixed_points); + + const auto constrain_marker_array = + getDebugConstrainMarkers(debug_data_ptr->constrain_rectangles, "constrain_rect"); + + appendMarkerArray(points_marker_array, &vis_marker_array); + appendMarkerArray(constrain_marker_array, &vis_marker_array); + + appendMarkerArray( + getRectanglesNumMarkerArray( + debug_data_ptr->mpt_traj, vehicle_param, "num_vehicle_footprint", 0.99, 0.99, 0.2), + &vis_marker_array); + + appendMarkerArray( + getPointsTextMarkerArray(debug_data_ptr->eb_traj, "eb_traj_text", 0.99, 0.99, 0.2), + &vis_marker_array); + } + + // avoiding objects + appendMarkerArray( + getObjectsMarkerArray(debug_data_ptr->avoiding_objects, "avoiding_objects", 0.99, 0.99, 0.2), + &vis_marker_array); + // mpt footprints + appendMarkerArray( + getRectanglesMarkerArray( + debug_data_ptr->mpt_traj, vehicle_param, "mpt_footprints", 0.99, 0.99, 0.2, + debug_data_ptr->mpt_visualize_sampling_num), + &vis_marker_array); + // bounds + appendMarkerArray( + getBoundsLineMarkerArray( + debug_data_ptr->ref_points, 0.99, 0.99, 0.2, debug_data_ptr->vehicle_circle_radius, + debug_data_ptr->mpt_visualize_sampling_num), + &vis_marker_array); + + /* + // bounds candidates + appendMarkerArray( + getBoundsCandidatesLineMarkerArray( + debug_data_ptr->ref_points, debug_data_ptr->sequential_bounds_candidates, 0.2, 0.99, 0.99, + debug_data_ptr->vehicle_circle_radius, debug_data_ptr->mpt_visualize_sampling_num), + &vis_marker_array); + */ + + // vehicle circle line + appendMarkerArray( + getVehicleCircleLineMarkerArray( + debug_data_ptr->vehicle_circles_pose, debug_data_ptr->vehicle_circle_radius, + debug_data_ptr->mpt_visualize_sampling_num, "vehicle_circle_lines", 0.99, 0.99, 0.2), + &vis_marker_array); + + // lateral error line + appendMarkerArray( + getLateralErrorsLineMarkerArray( + debug_data_ptr->mpt_ref_poses, debug_data_ptr->lateral_errors, + debug_data_ptr->mpt_visualize_sampling_num, "lateral_errors", 0.1, 0.1, 0.8), + &vis_marker_array); + + // current vehicle circles + appendMarkerArray( + getCurrentVehicleCirclesMarkerArray( + debug_data_ptr->current_ego_pose, debug_data_ptr->vehicle_circle_longitudinal_offsets, + debug_data_ptr->vehicle_circle_radius, "current_vehicle_circles", 1.0, 0.3, 0.3), + &vis_marker_array); + + // vehicle circles + appendMarkerArray( + getVehicleCirclesMarkerArray( + debug_data_ptr->mpt_traj, debug_data_ptr->vehicle_circle_longitudinal_offsets, + debug_data_ptr->vehicle_circle_radius, debug_data_ptr->mpt_visualize_sampling_num, + "vehicle_circles", 1.0, 0.3, 0.3), + &vis_marker_array); + + return vis_marker_array; +} + +nav_msgs::msg::OccupancyGrid getDebugCostmap( + const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid) +{ + if (clearance_map.empty()) return nav_msgs::msg::OccupancyGrid(); + + cv::Mat tmp; + clearance_map.copyTo(tmp); + cv::normalize(tmp, tmp, 0, 255, cv::NORM_MINMAX, CV_8UC1); + nav_msgs::msg::OccupancyGrid clearance_map_in_og = occupancy_grid; + tmp.forEach([&](const unsigned char & value, const int * position) -> void { + cv_utils::putOccupancyGridValue(clearance_map_in_og, position[0], position[1], value); + }); + return clearance_map_in_og; +} +} // namespace debug_visualization diff --git a/planning/obstacle_avoidance_planner/src/eb_path_optimizer.cpp b/planning/obstacle_avoidance_planner/src/eb_path_optimizer.cpp index 57dcad29a7c84..6743bc7884e1a 100644 --- a/planning/obstacle_avoidance_planner/src/eb_path_optimizer.cpp +++ b/planning/obstacle_avoidance_planner/src/eb_path_optimizer.cpp @@ -14,25 +14,9 @@ #include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/mpt_optimizer.hpp" -#include "obstacle_avoidance_planner/process_cv.hpp" -#include "obstacle_avoidance_planner/util.hpp" +#include "obstacle_avoidance_planner/utils.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include +#include "geometry_msgs/msg/vector3.hpp" #include #include @@ -41,66 +25,46 @@ #include EBPathOptimizer::EBPathOptimizer( - const bool is_showing_debug_info, const QPParam qp_param, const TrajectoryParam traj_pram, - const ConstrainParam constrain_param, const VehicleParam & vehicle_param, - const MPTParam & mpt_param) -: logger_ros_clock_(RCL_ROS_TIME), - is_showing_debug_info_(is_showing_debug_info), + const bool is_showing_debug_info, const TrajectoryParam & traj_param, const EBParam & eb_param, + const VehicleParam & vehicle_param) +: is_showing_debug_info_(is_showing_debug_info), epsilon_(1e-8), - qp_param_(qp_param), - traj_param_(traj_pram), - constrain_param_(constrain_param), + qp_param_(eb_param.qp_param), + traj_param_(traj_param), + eb_param_(eb_param), vehicle_param_(vehicle_param) { - geometry_msgs::msg::Vector3 keep_space_shape; - keep_space_shape.x = constrain_param_.keep_space_shape_x; - keep_space_shape.y = constrain_param_.keep_space_shape_y; - keep_space_shape_ptr_ = std::make_unique(keep_space_shape); - mpt_optimizer_ptr_ = std::make_unique( - is_showing_debug_info, qp_param, traj_pram, constrain_param, vehicle_param, mpt_param); - initializeSolver(); -} - -EBPathOptimizer::~EBPathOptimizer() {} - -void EBPathOptimizer::initializeSolver() -{ - Eigen::MatrixXd p = makePMatrix(); + const Eigen::MatrixXd p = makePMatrix(); default_a_matrix_ = makeAMatrix(); - std::vector q(traj_param_.num_sampling_points * 2, 0.0); - std::vector lower_bound(traj_param_.num_sampling_points * 2, 0.0); - std::vector upper_bound(traj_param_.num_sampling_points * 2, 0.0); + const int num_points = eb_param_.num_sampling_points_for_eb; + const std::vector q(num_points * 2, 0.0); + const std::vector lower_bound(num_points * 2, 0.0); + const std::vector upper_bound(num_points * 2, 0.0); + osqp_solver_ptr_ = std::make_unique( p, default_a_matrix_, q, lower_bound, upper_bound, qp_param_.eps_abs); osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel); osqp_solver_ptr_->updateMaxIter(qp_param_.max_iteration); - ex_osqp_solver_ptr_ = std::make_unique( - p, default_a_matrix_, q, lower_bound, upper_bound, qp_param_.eps_abs); - ex_osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel); - ex_osqp_solver_ptr_->updateMaxIter(qp_param_.max_iteration); - vis_osqp_solver_ptr_ = std::make_unique( - p, default_a_matrix_, q, lower_bound, upper_bound, qp_param_.eps_abs); - vis_osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel); - vis_osqp_solver_ptr_->updateMaxIter(qp_param_.max_iteration); } -/* make positive semidefinite matrix for objective function - reference: https://ieeexplore.ieee.org/document/7402333 */ +// make positive semidefinite matrix for objective function +// reference: https://ieeexplore.ieee.org/document/7402333 Eigen::MatrixXd EBPathOptimizer::makePMatrix() { - Eigen::MatrixXd P = - Eigen::MatrixXd::Zero(traj_param_.num_sampling_points * 2, traj_param_.num_sampling_points * 2); - for (int r = 0; r < traj_param_.num_sampling_points * 2; r++) { - for (int c = 0; c < traj_param_.num_sampling_points * 2; c++) { + const int num_points = eb_param_.num_sampling_points_for_eb; + + Eigen::MatrixXd P = Eigen::MatrixXd::Zero(num_points * 2, num_points * 2); + for (int r = 0; r < num_points * 2; ++r) { + for (int c = 0; c < num_points * 2; ++c) { if (r == c) { - P(r, c) = 6; + P(r, c) = 6.0; } else if (std::abs(c - r) == 1) { - P(r, c) = -4; + P(r, c) = -4.0; } else if (std::abs(c - r) == 2) { - P(r, c) = 1; + P(r, c) = 1.0; } else { - P(r, c) = 0; + P(r, c) = 0.0; } } } @@ -110,37 +74,33 @@ Eigen::MatrixXd EBPathOptimizer::makePMatrix() // make default linear constrain matrix Eigen::MatrixXd EBPathOptimizer::makeAMatrix() { - Eigen::MatrixXd A = Eigen::MatrixXd::Identity( - traj_param_.num_sampling_points * 2, traj_param_.num_sampling_points * 2); - for (int i = 0; i < traj_param_.num_sampling_points * 2; i++) { - if (i < traj_param_.num_sampling_points) { - A(i, i + traj_param_.num_sampling_points) = 1; + const int num_points = eb_param_.num_sampling_points_for_eb; + + Eigen::MatrixXd A = Eigen::MatrixXd::Identity(num_points * 2, num_points * 2); + for (int i = 0; i < num_points * 2; ++i) { + if (i < num_points) { + A(i, i + num_points) = 1.0; } else { - A(i, i - traj_param_.num_sampling_points) = 1; + A(i, i - num_points) = 1.0; } } return A; } -boost::optional EBPathOptimizer::generateOptimizedTrajectory( - const bool enable_avoidance, const geometry_msgs::msg::Pose & ego_pose, - const autoware_auto_planning_msgs::msg::Path & path, - const std::unique_ptr & prev_trajs, - const std::vector & objects, - DebugData * debug_data) +boost::optional> +EBPathOptimizer::getEBTrajectory( + const geometry_msgs::msg::Pose & ego_pose, const autoware_auto_planning_msgs::msg::Path & path, + const std::unique_ptr & prev_trajs, const double current_ego_vel, + std::shared_ptr debug_data_ptr) { - // processing drivable area - auto t_start1 = std::chrono::high_resolution_clock::now(); - CVMaps cv_maps = process_cv::getMaps(enable_avoidance, path, objects, traj_param_, debug_data); - auto t_end1 = std::chrono::high_resolution_clock::now(); - float elapsed_ms1 = std::chrono::duration(t_end1 - t_start1).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Processing driveable area time: = %f [ms]", elapsed_ms1); + stop_watch_.tic(__func__); + + current_ego_vel_ = current_ego_vel; // get candidate points for optimization - CandidatePoints candidate_points = getCandidatePoints( - ego_pose, path.points, prev_trajs, cv_maps.drivable_area, path.drivable_area.info, debug_data); + // decide fix or non fix, might not required only for smoothing purpose + const CandidatePoints candidate_points = + getCandidatePoints(ego_pose, path.points, prev_trajs, debug_data_ptr); if (candidate_points.fixed_points.empty() && candidate_points.non_fixed_points.empty()) { RCLCPP_INFO_EXPRESSION( rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, @@ -148,231 +108,115 @@ boost::optional EBPathOptimizer::generateOptimizedTrajectory( return boost::none; } - // get optimized smooth points - const auto opt_traj_points = getOptimizedTrajectory( - enable_avoidance, path, candidate_points, cv_maps.clearance_map, - cv_maps.only_objects_clearance_map, debug_data); - if (!opt_traj_points) { + // get optimized smooth points with elastic band + const auto eb_traj_points = getOptimizedTrajectory(path, candidate_points, debug_data_ptr); + if (!eb_traj_points) { RCLCPP_INFO_EXPRESSION( rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, "return boost::none since smoothing failed"); return boost::none; } - const auto mpt_trajs = mpt_optimizer_ptr_->getModelPredictiveTrajectory( - enable_avoidance, opt_traj_points.get(), path.points, prev_trajs, cv_maps, ego_pose, - debug_data); - if (!mpt_trajs) { - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "return boost::none since mpt failed"); - return boost::none; - } - - auto t_start2 = std::chrono::high_resolution_clock::now(); - std::vector extended_traj_points = - getExtendedOptimizedTrajectory(path.points, mpt_trajs.get().mpt, debug_data); - auto t_end2 = std::chrono::high_resolution_clock::now(); - float elapsed_ms2 = std::chrono::duration(t_end2 - t_start2).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Extending trajectory time: = %f [ms]", elapsed_ms2); - - Trajectories traj; - traj.smoothed_trajectory = opt_traj_points.get(); - traj.mpt_ref_points = mpt_trajs.get().ref_points; - traj.model_predictive_trajectory = mpt_trajs.get().mpt; - traj.extended_trajectory = extended_traj_points; - debug_data->smoothed_points = opt_traj_points.get(); - return traj; + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return eb_traj_points; } boost::optional> EBPathOptimizer::getOptimizedTrajectory( - [[maybe_unused]] const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & path, - const CandidatePoints & candidate_points, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, DebugData * debug_data) + const autoware_auto_planning_msgs::msg::Path & path, const CandidatePoints & candidate_points, + std::shared_ptr debug_data_ptr) { + stop_watch_.tic(__func__); + // get constrain rectangles around each point - std::vector interpolated_points = util::getInterpolatedPoints( - candidate_points.fixed_points, candidate_points.non_fixed_points, - traj_param_.delta_arc_length_for_optimization); + auto full_points = candidate_points.fixed_points; + full_points.insert( + full_points.end(), candidate_points.non_fixed_points.begin(), + candidate_points.non_fixed_points.end()); + + // interpolate points for logic purpose + const std::vector interpolated_points = + interpolation_utils::getInterpolatedPoints(full_points, eb_param_.delta_arc_length_for_eb); if (interpolated_points.empty()) { return boost::none; } - debug_data->interpolated_points = interpolated_points; + debug_data_ptr->interpolated_points = interpolated_points; + // number of optimizing points const int farthest_idx = std::min( - (traj_param_.num_sampling_points - 1), static_cast(interpolated_points.size() - 1)); + (eb_param_.num_sampling_points_for_eb - 1), static_cast(interpolated_points.size() - 1)); + // number of fixed points in interpolated_points const int num_fixed_points = getNumFixedPoints(candidate_points.fixed_points, interpolated_points, farthest_idx); - const int straight_line_idx = getStraightLineIdx( - interpolated_points, farthest_idx, only_objects_clearance_map, path.drivable_area.info, - debug_data->straight_points); + // TODO(murooka) try this printing. something may be wrong + // std::cerr << num_fixed_points << std::endl; + + // consider straight after `straight_line_idx` and then tighten space for optimization after + // `straight_line_idx` + const int straight_line_idx = + getStraightLineIdx(interpolated_points, farthest_idx, debug_data_ptr->straight_points); + + // if `farthest_idx` is lower than `number_of_sampling_points`, duplicate the point at the end of + // `interpolated_points` + // This aims for using hotstart by not changing the size of matrix std::vector padded_interpolated_points = getPaddedInterpolatedPoints(interpolated_points, farthest_idx); - auto t_start1 = std::chrono::high_resolution_clock::now(); + const auto rectangles = getConstrainRectangleVec( - path, padded_interpolated_points, num_fixed_points, farthest_idx, straight_line_idx, - clearance_map, only_objects_clearance_map); - auto t_end1 = std::chrono::high_resolution_clock::now(); - float elapsed_ms1 = std::chrono::duration(t_end1 - t_start1).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Make constrain rectangle time: = %f [ms]", elapsed_ms1); + path, padded_interpolated_points, num_fixed_points, farthest_idx, straight_line_idx); if (!rectangles) { return boost::none; } - const auto traj_points = calculateTrajectory( - padded_interpolated_points, rectangles.get(), farthest_idx, OptMode::Normal); - debug_data->smoothed_points = traj_points; - return traj_points; -} + const auto traj_points = + calculateTrajectory(padded_interpolated_points, rectangles.get(), farthest_idx, debug_data_ptr); -std::vector -EBPathOptimizer::getExtendedOptimizedTrajectory( - const std::vector & path_points, - const std::vector & optimized_points, - DebugData * debug_data) -{ - if (static_cast(optimized_points.size()) <= traj_param_.num_fix_points_for_extending) { - RCLCPP_INFO_THROTTLE( - rclcpp::get_logger("EBPathOptimizer"), logger_ros_clock_, - std::chrono::milliseconds(10000).count(), "[Avoidance] Not extend trajectory"); - return std::vector{}; - } - const double accum_arc_length = util::getArcLength(optimized_points); - if ( - accum_arc_length > traj_param_.trajectory_length || - util::getLastExtendedPoint( - path_points.back(), optimized_points.back().pose, - traj_param_.delta_yaw_threshold_for_closest_point, - traj_param_.max_dist_for_extending_end_point)) { - RCLCPP_INFO_THROTTLE( - rclcpp::get_logger("EBPathOptimizer"), logger_ros_clock_, - std::chrono::milliseconds(10000).count(), "[Avoidance] Not extend trajectory"); - return std::vector{}; - } - const int default_idx = -1; - int begin_idx = util::getNearestIdx( - path_points, optimized_points.back().pose, default_idx, - traj_param_.delta_yaw_threshold_for_closest_point); - if (begin_idx == default_idx) { - RCLCPP_INFO_THROTTLE( - rclcpp::get_logger("EBPathOptimizer"), logger_ros_clock_, - std::chrono::milliseconds(10000).count(), - "[Avoidance] Not extend traj since could not find nearest idx from last opt point"); - return std::vector{}; - } - begin_idx = std::min( - begin_idx + traj_param_.num_offset_for_begin_idx, static_cast(path_points.size()) - 1); - const double extending_trajectory_length = traj_param_.trajectory_length - accum_arc_length; - const int end_extended_path_idx = - getEndPathIdx(path_points, begin_idx, extending_trajectory_length); - std::vector non_fixed_points; - for (int i = begin_idx; i <= end_extended_path_idx; i++) { - non_fixed_points.push_back(path_points[i].pose); - } - - std::vector fixed_points; - for (int i = traj_param_.num_fix_points_for_extending; i > 0; i--) { - fixed_points.push_back(optimized_points[optimized_points.size() - i].pose); - } - - const double delta_arc_length_for_extending_trajectory = std::fmax( - traj_param_.delta_arc_length_for_optimization, - static_cast(extending_trajectory_length / traj_param_.num_sampling_points)); - std::vector interpolated_points = util::getInterpolatedPoints( - fixed_points, non_fixed_points, delta_arc_length_for_extending_trajectory); - if (interpolated_points.empty()) { - RCLCPP_INFO_THROTTLE( - rclcpp::get_logger("EBPathOptimizer"), logger_ros_clock_, - std::chrono::milliseconds(1000).count(), - "[Avoidance] Not extend traj since empty interpolated points"); - return std::vector{}; - } - const int farthest_idx = std::min( - (traj_param_.num_sampling_points - 1), static_cast(interpolated_points.size() - 1)); - std::vector padded_interpolated_points = - getPaddedInterpolatedPoints(interpolated_points, farthest_idx); - const double arc_length = util::getArcLength(fixed_points); - const int num_fix_points = - static_cast(arc_length) / delta_arc_length_for_extending_trajectory; - std::vector constrain_rectangles = - getConstrainRectangleVec(path_points, padded_interpolated_points, num_fix_points, farthest_idx); - - const auto extended_traj_points = calculateTrajectory( - padded_interpolated_points, constrain_rectangles, farthest_idx, OptMode::Extending); - - const int default_extend_begin_idx = 0; - const int extend_begin_idx = util::getNearestIdxOverPoint( - extended_traj_points, optimized_points.back().pose, default_extend_begin_idx, - traj_param_.delta_yaw_threshold_for_closest_point); - - std::vector extended_traj; - for (std::size_t i = extend_begin_idx; i < extended_traj_points.size(); i++) { - extended_traj.push_back(extended_traj_points[i]); - } - - debug_data->fixed_points_for_extending = fixed_points; - debug_data->non_fixed_points_for_extending = non_fixed_points; - debug_data->constrain_rectangles_for_extending = constrain_rectangles; - debug_data->interpolated_points_for_extending = interpolated_points; - return extended_traj; + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return traj_points; } std::vector EBPathOptimizer::calculateTrajectory( const std::vector & padded_interpolated_points, const std::vector & constrain_rectangles, const int farthest_idx, - const OptMode & opt_mode) + std::shared_ptr debug_data_ptr) { + stop_watch_.tic(__func__); + // update constrain for QP based on constrain rectangles - updateConstrain(padded_interpolated_points, constrain_rectangles, opt_mode); + updateConstrain(padded_interpolated_points, constrain_rectangles); // solve QP and get optimized trajectory - auto t_start2 = std::chrono::high_resolution_clock::now(); - std::vector optimized_points = solveQP(opt_mode); - auto t_end2 = std::chrono::high_resolution_clock::now(); - float elapsed_ms2 = std::chrono::duration(t_end2 - t_start2).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, "Optimization time: = %f [ms]", - elapsed_ms2); - auto traj_points = + std::vector optimized_points = solveQP(); + + const auto traj_points = convertOptimizedPointsToTrajectory(optimized_points, constrain_rectangles, farthest_idx); + + if (debug_data_ptr) { + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + } return traj_points; } -std::vector EBPathOptimizer::solveQP(const OptMode & opt_mode) +std::vector EBPathOptimizer::solveQP() { - std::vector optimized_points; - if (opt_mode == OptMode::Normal) { - osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel); - osqp_solver_ptr_->updateEpsAbs(qp_param_.eps_abs); - auto result = osqp_solver_ptr_->optimize(); - optimized_points = std::get<0>(result); - util::logOSQPSolutionStatus(std::get<3>(result)); - } else if (opt_mode == OptMode::Extending) { - ex_osqp_solver_ptr_->updateEpsAbs(qp_param_.eps_abs_for_extending); - ex_osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel_for_extending); - auto result = ex_osqp_solver_ptr_->optimize(); - optimized_points = std::get<0>(result); - util::logOSQPSolutionStatus(std::get<3>(result)); - } else if (opt_mode == OptMode::Visualizing) { - vis_osqp_solver_ptr_->updateEpsAbs(qp_param_.eps_abs_for_visualizing); - vis_osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel_for_visualizing); - auto result = vis_osqp_solver_ptr_->optimize(); - optimized_points = std::get<0>(result); - util::logOSQPSolutionStatus(std::get<3>(result)); - } + osqp_solver_ptr_->updateEpsRel(qp_param_.eps_rel); + osqp_solver_ptr_->updateEpsAbs(qp_param_.eps_abs); + + const auto result = osqp_solver_ptr_->optimize(); + const auto optimized_points = std::get<0>(result); + + utils::logOSQPSolutionStatus(std::get<3>(result)); + return optimized_points; } std::vector EBPathOptimizer::getFixedPoints( const geometry_msgs::msg::Pose & ego_pose, [[maybe_unused]] const std::vector & path_points, - const std::unique_ptr & prev_trajs, [[maybe_unused]] const cv::Mat & drivable_area, - [[maybe_unused]] const nav_msgs::msg::MapMetaData & map_info) + const std::unique_ptr & prev_trajs) { /* use of prev_traj_points(fine resolution) instead of prev_opt_traj(coarse resolution) stabilize trajectory's yaw*/ @@ -381,19 +225,25 @@ std::vector EBPathOptimizer::getFixedPoints( std::vector empty_points; return empty_points; } - const int default_idx = 0; - int begin_idx = util::getNearestIdx( - prev_trajs->smoothed_trajectory, ego_pose, default_idx, + const auto opt_begin_idx = tier4_autoware_utils::findNearestIndex( + prev_trajs->smoothed_trajectory, ego_pose, std::numeric_limits::max(), traj_param_.delta_yaw_threshold_for_closest_point); + const int begin_idx = opt_begin_idx ? *opt_begin_idx : 0; const int backward_fixing_idx = std::max( static_cast( begin_idx - traj_param_.backward_fixing_distance / traj_param_.delta_arc_length_for_trajectory), 0); + + // NOTE: Fixed length of EB has to be longer than that of MPT. + constexpr double forward_fixed_length_margin = 5.0; + const double forward_fixed_length = std::max( + current_ego_vel_ * traj_param_.forward_fixing_min_time + forward_fixed_length_margin, + traj_param_.forward_fixing_min_distance); + const int forward_fixing_idx = std::min( static_cast( - begin_idx + - traj_param_.forward_fixing_distance / traj_param_.delta_arc_length_for_trajectory), + begin_idx + forward_fixed_length / traj_param_.delta_arc_length_for_trajectory), static_cast(prev_trajs->smoothed_trajectory.size() - 1)); std::vector fixed_points; for (int i = backward_fixing_idx; i <= forward_fixing_idx; i++) { @@ -406,31 +256,33 @@ std::vector EBPathOptimizer::getFixedPoints( } } -CandidatePoints EBPathOptimizer::getCandidatePoints( +EBPathOptimizer::CandidatePoints EBPathOptimizer::getCandidatePoints( const geometry_msgs::msg::Pose & ego_pose, const std::vector & path_points, - const std::unique_ptr & prev_trajs, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info, DebugData * debug_data) + const std::unique_ptr & prev_trajs, std::shared_ptr debug_data_ptr) { const std::vector fixed_points = - getFixedPoints(ego_pose, path_points, prev_trajs, drivable_area, map_info); + getFixedPoints(ego_pose, path_points, prev_trajs); if (fixed_points.empty()) { CandidatePoints candidate_points = getDefaultCandidatePoints(path_points); return candidate_points; } - const int default_idx = -1; - int begin_idx = util::getNearestIdx( - path_points, fixed_points.back(), default_idx, + + // try to find non-fix points + const auto opt_begin_idx = tier4_autoware_utils::findNearestIndex( + path_points, fixed_points.back(), std::numeric_limits::max(), traj_param_.delta_yaw_threshold_for_closest_point); - if (begin_idx == default_idx) { + if (!opt_begin_idx) { CandidatePoints candidate_points; candidate_points.fixed_points = fixed_points; candidate_points.begin_path_idx = path_points.size(); candidate_points.end_path_idx = path_points.size(); return candidate_points; } - begin_idx = std::min( - begin_idx + traj_param_.num_offset_for_begin_idx, static_cast(path_points.size()) - 1); + + const int begin_idx = std::min( + static_cast(opt_begin_idx.get()) + eb_param_.num_offset_for_begin_idx, + static_cast(path_points.size()) - 1); std::vector non_fixed_points; for (size_t i = begin_idx; i < path_points.size(); i++) { @@ -442,8 +294,8 @@ CandidatePoints EBPathOptimizer::getCandidatePoints( candidate_points.begin_path_idx = begin_idx; candidate_points.end_path_idx = path_points.size() - 1; - debug_data->fixed_points = candidate_points.fixed_points; - debug_data->non_fixed_points = candidate_points.non_fixed_points; + debug_data_ptr->fixed_points = candidate_points.fixed_points; + debug_data_ptr->non_fixed_points = candidate_points.non_fixed_points; return candidate_points; } @@ -451,7 +303,7 @@ std::vector EBPathOptimizer::getPaddedInterpolatedPoi const std::vector & interpolated_points, const int farthest_point_idx) { std::vector padded_interpolated_points; - for (int i = 0; i < traj_param_.num_sampling_points; i++) { + for (int i = 0; i < eb_param_.num_sampling_points_for_eb; i++) { if (i > farthest_point_idx) { padded_interpolated_points.push_back(interpolated_points[farthest_point_idx]); } else { @@ -461,7 +313,7 @@ std::vector EBPathOptimizer::getPaddedInterpolatedPoi return padded_interpolated_points; } -CandidatePoints EBPathOptimizer::getDefaultCandidatePoints( +EBPathOptimizer::CandidatePoints EBPathOptimizer::getDefaultCandidatePoints( const std::vector & path_points) { double accum_arc_length = 0; @@ -469,12 +321,11 @@ CandidatePoints EBPathOptimizer::getDefaultCandidatePoints( std::vector fixed_points; for (size_t i = 0; i < path_points.size(); i++) { if (i > 0) { - accum_arc_length += - util::calculate2DDistance(path_points[i].pose.position, path_points[i - 1].pose.position); + accum_arc_length += tier4_autoware_utils::calcDistance2d( + path_points[i].pose.position, path_points[i - 1].pose.position); } if ( - accum_arc_length > - traj_param_.num_sampling_points * traj_param_.delta_arc_length_for_optimization) { + accum_arc_length > eb_param_.num_sampling_points_for_eb * eb_param_.delta_arc_length_for_eb) { break; } end_path_idx = i; @@ -487,99 +338,6 @@ CandidatePoints EBPathOptimizer::getDefaultCandidatePoints( return candidate_points; } -bool EBPathOptimizer::isPointInsideDrivableArea( - const geometry_msgs::msg::Point & point, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info) -{ - bool is_inside = true; - unsigned char occupancy_value = std::numeric_limits::max(); - const auto image_point = util::transformMapToOptionalImage(point, map_info); - if (image_point) { - occupancy_value = drivable_area.ptr( - static_cast(image_point.get().y))[static_cast(image_point.get().x)]; - } - if (!image_point || occupancy_value < epsilon_) { - is_inside = false; - } - return is_inside; -} - -int EBPathOptimizer::getEndPathIdx( - const std::vector & path_points, - const int begin_path_idx, const double required_trajectory_length) -{ - double accum_dist = 0; - int end_path_idx = begin_path_idx; - for (std::size_t i = begin_path_idx; i < path_points.size(); i++) { - if (static_cast(i) > begin_path_idx) { - const double dist = - util::calculate2DDistance(path_points[i].pose.position, path_points[i - 1].pose.position); - accum_dist += dist; - } - end_path_idx = i; - if (accum_dist > required_trajectory_length) { - break; - } - } - return end_path_idx; -} - -int EBPathOptimizer::getEndPathIdxInsideArea( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const int begin_path_idx, const cv::Mat & drivable_area, - const nav_msgs::msg::MapMetaData & map_info) -{ - int end_path_idx = path_points.size() - 1; - const int default_idx = 0; - const int initial_idx = util::getNearestIdx( - path_points, ego_pose, default_idx, traj_param_.delta_yaw_threshold_for_closest_point); - for (std::size_t i = initial_idx; i < path_points.size(); i++) { - geometry_msgs::msg::Point p = path_points[i].pose.position; - geometry_msgs::msg::Point top_image_point; - end_path_idx = i; - if (util::transformMapToImage(p, map_info, top_image_point)) { - const unsigned char value = drivable_area.ptr( - static_cast(top_image_point.y))[static_cast(top_image_point.x)]; - if (value < epsilon_) { - break; - } - } else { - break; - } - } - - int end_path_idx_inside_drivable_area = begin_path_idx; - for (int i = end_path_idx; i >= begin_path_idx; i--) { - geometry_msgs::msg::Point rel_top_left_point; - rel_top_left_point.x = vehicle_param_.length; - rel_top_left_point.y = vehicle_param_.width * 0.5; - geometry_msgs::msg::Point abs_top_left_point = - util::transformToAbsoluteCoordinate2D(rel_top_left_point, path_points[i].pose); - geometry_msgs::msg::Point top_left_image_point; - - geometry_msgs::msg::Point rel_top_right_point; - rel_top_right_point.x = vehicle_param_.length; - rel_top_right_point.y = -vehicle_param_.width * 0.5; - geometry_msgs::msg::Point abs_top_right_point = - util::transformToAbsoluteCoordinate2D(rel_top_right_point, path_points[i].pose); - geometry_msgs::msg::Point top_right_image_point; - if ( - util::transformMapToImage(abs_top_left_point, map_info, top_left_image_point) && - util::transformMapToImage(abs_top_right_point, map_info, top_right_image_point)) { - const unsigned char top_left_occupancy_value = drivable_area.ptr( - static_cast(top_left_image_point.y))[static_cast(top_left_image_point.x)]; - const unsigned char top_right_occupancy_value = drivable_area.ptr( - static_cast(top_right_image_point.y))[static_cast(top_right_image_point.x)]; - if (top_left_occupancy_value > epsilon_ && top_right_occupancy_value > epsilon_) { - end_path_idx_inside_drivable_area = i; - break; - } - } - } - return end_path_idx_inside_drivable_area; -} - int EBPathOptimizer::getNumFixedPoints( const std::vector & fixed_points, const std::vector & interpolated_points, const int farthest_idx) @@ -587,7 +345,7 @@ int EBPathOptimizer::getNumFixedPoints( int num_fixed_points = 0; if (!fixed_points.empty() && !interpolated_points.empty()) { std::vector interpolated_points = - util::getInterpolatedPoints(fixed_points, traj_param_.delta_arc_length_for_optimization); + interpolation_utils::getInterpolatedPoints(fixed_points, eb_param_.delta_arc_length_for_eb); num_fixed_points = interpolated_points.size(); } num_fixed_points = std::min(num_fixed_points, farthest_idx); @@ -603,16 +361,16 @@ EBPathOptimizer::convertOptimizedPointsToTrajectory( for (int i = 0; i <= farthest_idx; i++) { autoware_auto_planning_msgs::msg::TrajectoryPoint tmp_point; tmp_point.pose.position.x = optimized_points[i]; - tmp_point.pose.position.y = optimized_points[i + traj_param_.num_sampling_points]; + tmp_point.pose.position.y = optimized_points[i + eb_param_.num_sampling_points_for_eb]; tmp_point.longitudinal_velocity_mps = constraints[i].velocity; traj_points.push_back(tmp_point); } for (size_t i = 0; i < traj_points.size(); i++) { if (i > 0) { - traj_points[i].pose.orientation = util::getQuaternionFromPoints( + traj_points[i].pose.orientation = geometry_utils::getQuaternionFromPoints( traj_points[i].pose.position, traj_points[i - 1].pose.position); } else if (i == 0 && traj_points.size() > 1) { - traj_points[i].pose.orientation = util::getQuaternionFromPoints( + traj_points[i].pose.orientation = geometry_utils::getQuaternionFromPoints( traj_points[i + 1].pose.position, traj_points[i].pose.position); } } @@ -622,402 +380,84 @@ EBPathOptimizer::convertOptimizedPointsToTrajectory( boost::optional> EBPathOptimizer::getConstrainRectangleVec( const autoware_auto_planning_msgs::msg::Path & path, const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx, const int straight_idx, - [[maybe_unused]] const cv::Mat & clearance_map, const cv::Mat & only_objects_clearance_map) + const int farthest_point_idx, const int straight_idx) { - const nav_msgs::msg::MapMetaData map_info = path.drivable_area.info; - std::vector smooth_constrain_rects(traj_param_.num_sampling_points); - for (int i = 0; i < traj_param_.num_sampling_points; i++) { + auto curvatures = points_utils::calcCurvature(interpolated_points, 10); + + std::vector smooth_constrain_rects(eb_param_.num_sampling_points_for_eb); + for (int i = 0; i < eb_param_.num_sampling_points_for_eb; i++) { + // `Anchor` has pose + velocity const Anchor anchor = getAnchor(interpolated_points, i, path.points); + + // four types of rectangle for various types if (i == 0 || i == 1 || i >= farthest_point_idx - 1 || i < num_fixed_points - 1) { - const auto rect = getConstrainRectangle(anchor, constrain_param_.clearance_for_fixing); - const auto updated_rect = getUpdatedConstrainRectangle( - rect, anchor.pose.position, map_info, only_objects_clearance_map); - smooth_constrain_rects[i] = updated_rect; + // rect has four points for a rectangle in map coordinate + const auto rect = getConstrainRectangle(anchor, eb_param_.clearance_for_fixing); + smooth_constrain_rects[i] = rect; } else if ( // NOLINT - i >= num_fixed_points - traj_param_.num_joint_buffer_points && - i <= num_fixed_points + traj_param_.num_joint_buffer_points) { - const auto rect = getConstrainRectangle(anchor, constrain_param_.clearance_for_joint); - const auto updated_rect = getUpdatedConstrainRectangle( - rect, anchor.pose.position, map_info, only_objects_clearance_map); - smooth_constrain_rects[i] = updated_rect; + i >= num_fixed_points - eb_param_.num_joint_buffer_points && + i <= num_fixed_points + eb_param_.num_joint_buffer_points) { + const auto rect = getConstrainRectangle(anchor, eb_param_.clearance_for_joint); + smooth_constrain_rects[i] = rect; } else if (i >= straight_idx) { - const auto rect = getConstrainRectangle(anchor, constrain_param_.clearance_for_straight_line); - const auto updated_rect = getUpdatedConstrainRectangle( - rect, anchor.pose.position, map_info, only_objects_clearance_map); - smooth_constrain_rects[i] = updated_rect; + const auto rect = getConstrainRectangle(anchor, eb_param_.clearance_for_straight_line); + smooth_constrain_rects[i] = rect; } else { - const auto rect = - getConstrainRectangle(anchor, constrain_param_.clearance_for_only_smoothing); - const auto updated_rect = getUpdatedConstrainRectangle( - rect, anchor.pose.position, map_info, only_objects_clearance_map); - smooth_constrain_rects[i] = updated_rect; + const double min_x = -eb_param_.clearance_for_only_smoothing; + const double max_x = eb_param_.clearance_for_only_smoothing; + const double min_y = curvatures[i] > 0 ? 0 : -eb_param_.clearance_for_only_smoothing; + const double max_y = curvatures[i] <= 0 ? 0 : eb_param_.clearance_for_only_smoothing; + const auto rect = getConstrainRectangle(anchor, min_x, max_x, min_y, max_y); + smooth_constrain_rects[i] = rect; } } return smooth_constrain_rects; } -boost::optional> EBPathOptimizer::getConstrainRectangleVec( - const bool enable_avoidance, const autoware_auto_planning_msgs::msg::Path & path, - const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx, const int straight_idx, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, DebugData * debug_data) -{ - const nav_msgs::msg::MapMetaData map_info = path.drivable_area.info; - std::vector object_road_constrain_ranges(traj_param_.num_sampling_points); - std::vector road_constrain_ranges(traj_param_.num_sampling_points); - std::vector only_smooth_constrain_ranges(traj_param_.num_sampling_points); - for (int i = 0; i < traj_param_.num_sampling_points; i++) { - const Anchor anchor = getAnchor(interpolated_points, i, path.points); - if (i == 0 || i == 1 || i >= farthest_point_idx - 1 || i < num_fixed_points - 1) { - const ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.clearance_for_fixing); - object_road_constrain_ranges[i] = getUpdatedConstrainRectangle( - rectangle, anchor.pose.position, map_info, only_objects_clearance_map); - road_constrain_ranges[i] = rectangle; - only_smooth_constrain_ranges[i] = rectangle; - } else { - if ( - i >= num_fixed_points - traj_param_.num_joint_buffer_points && - i <= num_fixed_points + traj_param_.num_joint_buffer_points) { - const ConstrainRectangle rectangle = - getConstrainRectangle(path.points, anchor, clearance_map, map_info); - object_road_constrain_ranges[i] = getUpdatedConstrainRectangle( - rectangle, anchor.pose.position, map_info, only_objects_clearance_map); - road_constrain_ranges[i] = rectangle; - only_smooth_constrain_ranges[i] = rectangle; - } else { - if (i >= straight_idx) { - const ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.clearance_for_straight_line); - object_road_constrain_ranges[i] = getUpdatedConstrainRectangle( - rectangle, anchor.pose.position, map_info, only_objects_clearance_map); - road_constrain_ranges[i] = rectangle; - only_smooth_constrain_ranges[i] = rectangle; - } else { - const ConstrainRectangles constrain_rectangles = - getConstrainRectangles(anchor, clearance_map, only_objects_clearance_map, map_info); - object_road_constrain_ranges[i] = constrain_rectangles.object_constrain_rectangle; - road_constrain_ranges[i] = constrain_rectangles.road_constrain_rectangle; - only_smooth_constrain_ranges[i] = - getConstrainRectangle(anchor, constrain_param_.clearance_for_only_smoothing); - } - } - } - } - debug_data->foa_data = - getFOAData(object_road_constrain_ranges, interpolated_points, farthest_point_idx); - boost::optional> constrain_ranges = - getPostProcessedConstrainRectangles( - enable_avoidance, object_road_constrain_ranges, road_constrain_ranges, - only_smooth_constrain_ranges, interpolated_points, path.points, farthest_point_idx, - num_fixed_points, straight_idx, debug_data); - return constrain_ranges; -} - -std::vector EBPathOptimizer::getConstrainRectangleVec( - const std::vector & path_points, - const std::vector & interpolated_points, const int num_fixed_points, - const int farthest_point_idx) -{ - std::vector only_smooth_constrain_ranges(traj_param_.num_sampling_points); - for (int i = 0; i < traj_param_.num_sampling_points; i++) { - const Anchor anchor = getAnchor(interpolated_points, i, path_points); - if (i < num_fixed_points || i >= farthest_point_idx - 1) { - ConstrainRectangle rectangle = getConstrainRectangle(anchor, 0); - only_smooth_constrain_ranges[i] = rectangle; - } else { - if ( - i >= num_fixed_points && - i <= num_fixed_points + traj_param_.num_joint_buffer_points_for_extending) { - ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.range_for_extend_joint); - only_smooth_constrain_ranges[i] = rectangle; - } else { - ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.clearance_for_only_smoothing); - only_smooth_constrain_ranges[i] = rectangle; - } - } - } - return only_smooth_constrain_ranges; -} - -boost::optional> -EBPathOptimizer::getPostProcessedConstrainRectangles( - const bool enable_avoidance, const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, - const std::vector & interpolated_points, - const std::vector & path_points, - const int farthest_point_idx, const int num_fixed_points, const int straight_idx, - DebugData * debug_data) const -{ - bool is_using_road_constrain = false; - if (!enable_avoidance) { - is_using_road_constrain = true; - } - bool is_using_only_smooth_constrain = false; - if (isFixingPathPoint(path_points)) { - is_using_only_smooth_constrain = true; - } - if (constrain_param_.is_getting_constraints_close2path_points) { - return getConstrainRectanglesClose2PathPoints( - is_using_only_smooth_constrain, is_using_road_constrain, object_constrains, road_constrains, - only_smooth_constrains, debug_data); - - } else { - return getConstrainRectanglesWithinArea( - is_using_only_smooth_constrain, is_using_road_constrain, farthest_point_idx, num_fixed_points, - straight_idx, object_constrains, road_constrains, only_smooth_constrains, interpolated_points, - path_points, debug_data); - } -} - -boost::optional> -EBPathOptimizer::getConstrainRectanglesClose2PathPoints( - const bool is_using_only_smooth_constrain, const bool is_using_road_constrain, - const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, DebugData * debug_data) const -{ - if (is_using_only_smooth_constrain) { - return only_smooth_constrains; - } - if (is_using_road_constrain) { - return getValidConstrainRectangles(road_constrains, only_smooth_constrains, debug_data); - } else { - return getValidConstrainRectangles(object_constrains, only_smooth_constrains, debug_data); - } -} - -boost::optional> EBPathOptimizer::getValidConstrainRectangles( - const std::vector & constrains, - const std::vector & only_smooth_constrains, DebugData * debug_data) const -{ - bool only_smooth = true; - int debug_cnt = 0; - for (const auto & rect : constrains) { - if (rect.is_empty_driveable_area) { - debug_data->constrain_rectangles = constrains; - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, "Constraint failed at %d", - debug_cnt); - return boost::none; - } - if (!rect.is_including_only_smooth_range) { - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Constraint does not include only smooth range at %d", debug_cnt); - only_smooth = false; - } - debug_cnt++; - } - if (only_smooth) { - debug_data->constrain_rectangles = only_smooth_constrains; - return only_smooth_constrains; - } else { - debug_data->constrain_rectangles = constrains; - return constrains; - } -} - -boost::optional> EBPathOptimizer::getConstrainRectanglesWithinArea( - bool is_using_only_smooth_constrain, bool is_using_road_constrain, const int farthest_point_idx, - const int num_fixed_points, const int straight_idx, - const std::vector & object_constrains, - const std::vector & road_constrains, - const std::vector & only_smooth_constrains, - const std::vector & interpolated_points, - const std::vector & path_points, - DebugData * debug_data) const -{ - if (is_using_road_constrain) { - debug_data->constrain_rectangles = road_constrains; - } else if (is_using_only_smooth_constrain) { - debug_data->constrain_rectangles = only_smooth_constrains; - } else { - debug_data->constrain_rectangles = object_constrains; - } - - std::vector constrain_ranges(traj_param_.num_sampling_points); - int origin_dynamic_joint_idx = traj_param_.num_sampling_points; - for (int i = 0; i < traj_param_.num_sampling_points; i++) { - if (isPreFixIdx(i, farthest_point_idx, num_fixed_points, straight_idx)) { - constrain_ranges[i] = only_smooth_constrains[i]; - if (object_constrains[i].is_empty_driveable_area) { - is_using_road_constrain = true; - return boost::none; - } - } else { - if ( - i > origin_dynamic_joint_idx && - i <= origin_dynamic_joint_idx + traj_param_.num_joint_buffer_points) { - const Anchor anchor = getAnchor(interpolated_points, i, path_points); - ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.clearance_for_joint); - constrain_ranges[i] = rectangle; - } else if (is_using_only_smooth_constrain) { - constrain_ranges[i] = only_smooth_constrains[i]; - } else if (is_using_road_constrain) { - constrain_ranges[i] = road_constrains[i]; - if (constrain_ranges[i].is_empty_driveable_area) { - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Only road clearance optimization failed at %d", i); - is_using_only_smooth_constrain = true; - origin_dynamic_joint_idx = i; - return boost::none; - } - } else { - constrain_ranges[i] = object_constrains[i]; - if (constrain_ranges[i].is_empty_driveable_area) { - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("EBPathOptimizer"), is_showing_debug_info_, - "Object clearance optimization failed at %d", i); - return boost::none; - } - } - } - } - return constrain_ranges; -} - -bool EBPathOptimizer::isPreFixIdx( - const int target_idx, const int farthest_point_idx, const int num_fixed_points, - const int straight_idx) const -{ - if ( - target_idx == 0 || target_idx == 1 || target_idx >= farthest_point_idx - 1 || - target_idx < num_fixed_points - 1 || - (target_idx >= num_fixed_points - traj_param_.num_joint_buffer_points && - target_idx <= num_fixed_points + traj_param_.num_joint_buffer_points) || - target_idx >= straight_idx) { - return true; - } else { - return false; - } -} - -bool EBPathOptimizer::isClose2Object( - const geometry_msgs::msg::Point & point, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & only_objects_clearance_map, const double distance_threshold) const -{ - const auto image_point = util::transformMapToOptionalImage(point, map_info); - if (!image_point) { - return false; - } - const float object_clearance = only_objects_clearance_map.ptr(static_cast( - image_point.get().y))[static_cast(image_point.get().x)] * - map_info.resolution; - if (object_clearance < distance_threshold) { - return true; - } - return false; -} - void EBPathOptimizer::updateConstrain( const std::vector & interpolated_points, - const std::vector & rectangle_points, const OptMode & opt_mode) + const std::vector & rectangle_points) { + const int num_points = eb_param_.num_sampling_points_for_eb; + Eigen::MatrixXd A = default_a_matrix_; - std::vector lower_bound(traj_param_.num_sampling_points * 2, 0.0); - std::vector upper_bound(traj_param_.num_sampling_points * 2, 0.0); - for (int i = 0; i < traj_param_.num_sampling_points; ++i) { + std::vector lower_bound(num_points * 2, 0.0); + std::vector upper_bound(num_points * 2, 0.0); + for (int i = 0; i < num_points; ++i) { Constrain constrain = getConstrainFromConstrainRectangle(interpolated_points[i], rectangle_points[i]); A(i, i) = constrain.top_and_bottom.x_coef; - A(i, i + traj_param_.num_sampling_points) = constrain.top_and_bottom.y_coef; - A(i + traj_param_.num_sampling_points, i) = constrain.left_and_right.x_coef; - A(i + traj_param_.num_sampling_points, i + traj_param_.num_sampling_points) = - constrain.left_and_right.y_coef; + A(i, i + num_points) = constrain.top_and_bottom.y_coef; + A(i + num_points, i) = constrain.left_and_right.x_coef; + A(i + num_points, i + num_points) = constrain.left_and_right.y_coef; lower_bound[i] = constrain.top_and_bottom.lower_bound; upper_bound[i] = constrain.top_and_bottom.upper_bound; - lower_bound[i + traj_param_.num_sampling_points] = constrain.left_and_right.lower_bound; - upper_bound[i + traj_param_.num_sampling_points] = constrain.left_and_right.upper_bound; - } - if (opt_mode == OptMode::Normal) { - osqp_solver_ptr_->updateBounds(lower_bound, upper_bound); - osqp_solver_ptr_->updateA(A); - } else if (opt_mode == OptMode::Extending) { - ex_osqp_solver_ptr_->updateBounds(lower_bound, upper_bound); - ex_osqp_solver_ptr_->updateA(A); - } else if (opt_mode == OptMode::Visualizing) { - vis_osqp_solver_ptr_->updateBounds(lower_bound, upper_bound); - vis_osqp_solver_ptr_->updateA(A); + lower_bound[i + num_points] = constrain.left_and_right.lower_bound; + upper_bound[i + num_points] = constrain.left_and_right.upper_bound; } -} - -Rectangle EBPathOptimizer::getAbsShapeRectangle( - const Rectangle & rel_shape_rectangle_points, const geometry_msgs::msg::Point & offset_point, - const geometry_msgs::msg::Pose & origin) const -{ - geometry_msgs::msg::Point abs_target_point = - util::transformToAbsoluteCoordinate2D(offset_point, origin); - - geometry_msgs::msg::Point abs_top_left; - abs_top_left.x = (rel_shape_rectangle_points.top_left.x + abs_target_point.x); - abs_top_left.y = (rel_shape_rectangle_points.top_left.y + abs_target_point.y); - - geometry_msgs::msg::Point abs_top_right; - abs_top_right.x = (rel_shape_rectangle_points.top_right.x + abs_target_point.x); - abs_top_right.y = (rel_shape_rectangle_points.top_right.y + abs_target_point.y); - - geometry_msgs::msg::Point abs_bottom_left; - abs_bottom_left.x = (rel_shape_rectangle_points.bottom_left.x + abs_target_point.x); - abs_bottom_left.y = (rel_shape_rectangle_points.bottom_left.y + abs_target_point.y); - - geometry_msgs::msg::Point abs_bottom_right; - abs_bottom_right.x = (rel_shape_rectangle_points.bottom_right.x + abs_target_point.x); - abs_bottom_right.y = (rel_shape_rectangle_points.bottom_right.y + abs_target_point.y); - Rectangle abs_shape_rectangle_points; - abs_shape_rectangle_points.top_left = abs_top_left; - abs_shape_rectangle_points.top_right = abs_top_right; - abs_shape_rectangle_points.bottom_left = abs_bottom_left; - abs_shape_rectangle_points.bottom_right = abs_bottom_right; - return abs_shape_rectangle_points; + osqp_solver_ptr_->updateBounds(lower_bound, upper_bound); + osqp_solver_ptr_->updateA(A); } -geometry_msgs::msg::Pose EBPathOptimizer::getOriginPose( - const std::vector & interpolated_points, const int interpolated_idx, - const std::vector & path_points) -{ - geometry_msgs::msg::Pose pose; - pose.position = interpolated_points[interpolated_idx]; - if (interpolated_idx > 0) { - pose.orientation = util::getQuaternionFromPoints( - interpolated_points[interpolated_idx], interpolated_points[interpolated_idx - 1]); - } else if (interpolated_idx == 0 && interpolated_points.size() > 1) { - pose.orientation = util::getQuaternionFromPoints( - interpolated_points[interpolated_idx + 1], interpolated_points[interpolated_idx]); - } - const int default_idx = 0; - const int nearest_id = util::getNearestIdx( - path_points, pose, default_idx, traj_param_.delta_yaw_threshold_for_closest_point); - const geometry_msgs::msg::Quaternion nearest_q = path_points[nearest_id].pose.orientation; - geometry_msgs::msg::Pose origin; - origin.position = interpolated_points[interpolated_idx]; - origin.orientation = nearest_q; - return origin; -} - -Anchor EBPathOptimizer::getAnchor( +EBPathOptimizer::Anchor EBPathOptimizer::getAnchor( const std::vector & interpolated_points, const int interpolated_idx, const std::vector & path_points) const { geometry_msgs::msg::Pose pose; pose.position = interpolated_points[interpolated_idx]; if (interpolated_idx > 0) { - pose.orientation = util::getQuaternionFromPoints( + pose.orientation = geometry_utils::getQuaternionFromPoints( interpolated_points[interpolated_idx], interpolated_points[interpolated_idx - 1]); } else if (interpolated_idx == 0 && interpolated_points.size() > 1) { - pose.orientation = util::getQuaternionFromPoints( + pose.orientation = geometry_utils::getQuaternionFromPoints( interpolated_points[interpolated_idx + 1], interpolated_points[interpolated_idx]); } - const int default_idx = 0; - const int nearest_idx = util::getNearestIdx( - path_points, pose, default_idx, traj_param_.delta_yaw_threshold_for_closest_point); + const auto opt_nearest_idx = tier4_autoware_utils::findNearestIndex( + path_points, pose, std::numeric_limits::max(), + traj_param_.delta_yaw_threshold_for_closest_point); + const int nearest_idx = opt_nearest_idx ? *opt_nearest_idx : 0; + const geometry_msgs::msg::Quaternion nearest_q = path_points[nearest_idx].pose.orientation; Anchor anchor; anchor.pose.position = interpolated_points[interpolated_idx]; @@ -1026,268 +466,26 @@ Anchor EBPathOptimizer::getAnchor( return anchor; } -boost::optional>> -EBPathOptimizer::getOccupancyPoints( - const geometry_msgs::msg::Pose & origin, const cv::Mat & clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const -{ - geometry_msgs::msg::Point image_point; - if (!util::transformMapToImage(origin.position, map_info, image_point)) { - return boost::none; - } - const float clearance = std::max( - clearance_map.ptr(static_cast(image_point.y))[static_cast(image_point.x)] * - map_info.resolution, - static_cast(keep_space_shape_ptr_->y)); - const float y_constrain_search_range = clearance - 0.5 * keep_space_shape_ptr_->y; - int y_side_length = 0; - for (float y = -y_constrain_search_range; y <= y_constrain_search_range + epsilon_; - y += map_info.resolution * constrain_param_.coef_y_constrain_search_resolution) { - y_side_length++; - } - const float x_constrain_search_range = - std::fmin(constrain_param_.max_x_constrain_search_range, y_constrain_search_range); - int x_side_length = 0; - for (float x = -1 * x_constrain_search_range; x <= x_constrain_search_range + epsilon_; - x += map_info.resolution * constrain_param_.coef_x_constrain_search_resolution) { - x_side_length++; - } - if (x_side_length == 0 || y_side_length == 0) { - return boost::none; - } - std::vector> occupancy_points( - x_side_length, std::vector(y_side_length)); - int x_idx_in_occupancy_map = 0; - int y_idx_in_occupancy_map = 0; - for (float x = -1 * x_constrain_search_range; x <= x_constrain_search_range + epsilon_; - x += map_info.resolution * constrain_param_.coef_x_constrain_search_resolution) { - for (float y = -1 * y_constrain_search_range; y <= y_constrain_search_range + epsilon_; - y += map_info.resolution * constrain_param_.coef_y_constrain_search_resolution) { - geometry_msgs::msg::Point relative_point; - relative_point.x = x; - relative_point.y = y; - geometry_msgs::msg::Point abs_point = - util::transformToAbsoluteCoordinate2D(relative_point, origin); - const int x_idx = x_side_length - x_idx_in_occupancy_map - 1; - const int y_idx = y_side_length - y_idx_in_occupancy_map - 1; - if (x_idx < 0 || x_idx >= x_side_length || y_idx < 0 || y_idx >= y_side_length) { - continue; - } - occupancy_points[x_idx][y_idx] = abs_point; - y_idx_in_occupancy_map++; - } - x_idx_in_occupancy_map++; - y_idx_in_occupancy_map = 0; - } - return occupancy_points; -} - -Rectangle EBPathOptimizer::getRelShapeRectangle( - const geometry_msgs::msg::Vector3 & vehicle_shape, const geometry_msgs::msg::Pose & origin) const -{ - geometry_msgs::msg::Point top_left; - top_left.x = vehicle_shape.x; - top_left.y = 0.5 * vehicle_shape.y; - geometry_msgs::msg::Point top_right; - top_right.x = vehicle_shape.x; - top_right.y = -0.5 * vehicle_shape.y; - geometry_msgs::msg::Point bottom_left; - bottom_left.x = 0.0; - bottom_left.y = 0.5 * vehicle_shape.y; - geometry_msgs::msg::Point bottom_right; - bottom_right.x = 0.0; - bottom_right.y = -0.5 * vehicle_shape.y; - - geometry_msgs::msg::Pose tmp_origin; - tmp_origin.orientation = origin.orientation; - top_left = util::transformToAbsoluteCoordinate2D(top_left, tmp_origin); - top_right = util::transformToAbsoluteCoordinate2D(top_right, tmp_origin); - bottom_left = util::transformToAbsoluteCoordinate2D(bottom_left, tmp_origin); - bottom_right = util::transformToAbsoluteCoordinate2D(bottom_right, tmp_origin); - Rectangle rectangle; - rectangle.top_left = top_left; - rectangle.top_right = top_right; - rectangle.bottom_left = bottom_left; - rectangle.bottom_right = bottom_right; - return rectangle; -} - -ConstrainRectangles EBPathOptimizer::getConstrainRectangles( - const Anchor & anchor, const cv::Mat & clearance_map, const cv::Mat & only_objects_clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const -{ - const auto occupancy_points_opt = getOccupancyPoints(anchor.pose, clearance_map, map_info); - const auto image_point = util::transformMapToOptionalImage(anchor.pose.position, map_info); - if (!image_point || !occupancy_points_opt) { - ConstrainRectangle rectangle = - getConstrainRectangle(anchor, constrain_param_.clearance_for_joint); - rectangle.is_empty_driveable_area = true; - ConstrainRectangles constrain_rectangles; - constrain_rectangles.object_constrain_rectangle = rectangle; - constrain_rectangles.road_constrain_rectangle = rectangle; - return constrain_rectangles; - } - OccupancyMaps occupancy_maps = getOccupancyMaps( - occupancy_points_opt.get(), anchor.pose, image_point.get(), clearance_map, - only_objects_clearance_map, map_info); - - ConstrainRectangle object_constrain = getConstrainRectangle( - occupancy_maps.object_occupancy_map, occupancy_points_opt.get(), anchor, map_info, - only_objects_clearance_map); - - ConstrainRectangles constrain_rectangles; - constrain_rectangles.object_constrain_rectangle = getUpdatedConstrainRectangle( - object_constrain, anchor.pose.position, map_info, only_objects_clearance_map); - constrain_rectangles.road_constrain_rectangle = getConstrainRectangle( - occupancy_maps.road_occupancy_map, occupancy_points_opt.get(), anchor, map_info, - only_objects_clearance_map); - return constrain_rectangles; -} - -OccupancyMaps EBPathOptimizer::getOccupancyMaps( - const std::vector> & occupancy_points, - const geometry_msgs::msg::Pose & origin_pose, - const geometry_msgs::msg::Point & origin_point_in_image, const cv::Mat & clearance_map, - const cv::Mat & only_objects_clearance_map, const nav_msgs::msg::MapMetaData & map_info) const -{ - Rectangle rel_shape_rectangles = getRelShapeRectangle(*keep_space_shape_ptr_, origin_pose); - const float clearance = std::max( - clearance_map.ptr( - static_cast(origin_point_in_image.y))[static_cast(origin_point_in_image.x)] * - map_info.resolution, - static_cast(keep_space_shape_ptr_->y)); - const float y_constrain_search_range = clearance - 0.5 * keep_space_shape_ptr_->y; - const float x_constrain_search_range = - std::fmin(constrain_param_.max_x_constrain_search_range, y_constrain_search_range); - std::vector> object_occupancy_map( - occupancy_points.size(), std::vector(occupancy_points.front().size(), 0)); - std::vector> road_occupancy_map( - occupancy_points.size(), std::vector(occupancy_points.front().size(), 0)); - int x_idx_in_occupancy_map = 0; - int y_idx_in_occupancy_map = 0; - for (float x = -1 * x_constrain_search_range; x <= x_constrain_search_range + epsilon_; - x += map_info.resolution * constrain_param_.coef_x_constrain_search_resolution) { - for (float y = -1 * y_constrain_search_range; y <= y_constrain_search_range + epsilon_; - y += map_info.resolution * constrain_param_.coef_y_constrain_search_resolution) { - geometry_msgs::msg::Point rel_target_point; - rel_target_point.x = x; - rel_target_point.y = y; - Rectangle abs_shape_rectangles = - getAbsShapeRectangle(rel_shape_rectangles, rel_target_point, origin_pose); - float top_left_clearance = std::numeric_limits::lowest(); - float top_left_objects_clearance = std::numeric_limits::lowest(); - geometry_msgs::msg::Point top_left_image; - if (util::transformMapToImage(abs_shape_rectangles.top_left, map_info, top_left_image)) { - top_left_clearance = clearance_map.ptr(static_cast( - top_left_image.y))[static_cast(top_left_image.x)] * - map_info.resolution; - top_left_objects_clearance = only_objects_clearance_map.ptr(static_cast( - top_left_image.y))[static_cast(top_left_image.x)] * - map_info.resolution; - } - - float top_right_clearance = std::numeric_limits::lowest(); - float top_right_objects_clearance = std::numeric_limits::lowest(); - geometry_msgs::msg::Point top_right_image; - if (util::transformMapToImage(abs_shape_rectangles.top_right, map_info, top_right_image)) { - top_right_clearance = clearance_map.ptr(static_cast( - top_right_image.y))[static_cast(top_right_image.x)] * - map_info.resolution; - top_right_objects_clearance = only_objects_clearance_map.ptr(static_cast( - top_right_image.y))[static_cast(top_right_image.x)] * - map_info.resolution; - } - float bottom_left_clearance = std::numeric_limits::lowest(); - float bottom_left_objects_clearance = std::numeric_limits::lowest(); - geometry_msgs::msg::Point bottom_left_image; - if (util::transformMapToImage( - abs_shape_rectangles.bottom_left, map_info, bottom_left_image)) { - bottom_left_clearance = clearance_map.ptr(static_cast( - bottom_left_image.y))[static_cast(bottom_left_image.x)] * - map_info.resolution; - bottom_left_objects_clearance = - only_objects_clearance_map.ptr( - static_cast(bottom_left_image.y))[static_cast(bottom_left_image.x)] * - map_info.resolution; - } - float bottom_right_clearance = std::numeric_limits::lowest(); - float bottom_right_objects_clearance = std::numeric_limits::lowest(); - geometry_msgs::msg::Point bottom_right_image; - if (util::transformMapToImage( - abs_shape_rectangles.bottom_right, map_info, bottom_right_image)) { - bottom_right_clearance = clearance_map.ptr(static_cast( - bottom_right_image.y))[static_cast(bottom_right_image.x)] * - map_info.resolution; - bottom_right_objects_clearance = - only_objects_clearance_map.ptr( - static_cast(bottom_right_image.y))[static_cast(bottom_right_image.x)] * - map_info.resolution; - } - - const int x_idx = occupancy_points.size() - x_idx_in_occupancy_map - 1; - const int y_idx = occupancy_points.front().size() - y_idx_in_occupancy_map - 1; - if ( - x_idx < 0 || x_idx >= static_cast(occupancy_points.size()) || y_idx < 0 || - y_idx >= static_cast(occupancy_points.front().size())) { - continue; - } - if ( - top_left_clearance < constrain_param_.clearance_from_road || - top_right_clearance < constrain_param_.clearance_from_road || - bottom_right_clearance < constrain_param_.clearance_from_road || - bottom_left_clearance < constrain_param_.clearance_from_road || - top_left_objects_clearance < constrain_param_.clearance_from_object || - top_right_objects_clearance < constrain_param_.clearance_from_object || - bottom_right_objects_clearance < constrain_param_.clearance_from_object || - bottom_left_objects_clearance < constrain_param_.clearance_from_object) { - object_occupancy_map[x_idx][y_idx] = 1; - } - if ( - top_left_clearance < constrain_param_.clearance_from_road || - top_right_clearance < constrain_param_.clearance_from_road || - bottom_right_clearance < constrain_param_.clearance_from_road || - bottom_left_clearance < constrain_param_.clearance_from_road) { - road_occupancy_map[x_idx][y_idx] = 1; - } - y_idx_in_occupancy_map++; - } - x_idx_in_occupancy_map++; - y_idx_in_occupancy_map = 0; - } - OccupancyMaps occupancy_maps; - occupancy_maps.object_occupancy_map = object_occupancy_map; - occupancy_maps.road_occupancy_map = road_occupancy_map; - return occupancy_maps; -} - int EBPathOptimizer::getStraightLineIdx( const std::vector & interpolated_points, const int farthest_point_idx, - const cv::Mat & only_objects_clearance_map, const nav_msgs::msg::MapMetaData & map_info, std::vector & debug_detected_straight_points) { double prev_yaw = 0; int straight_line_idx = farthest_point_idx; for (int i = farthest_point_idx; i >= 0; i--) { if (i < farthest_point_idx) { - const double yaw = util::getYawFromPoints(interpolated_points[i + 1], interpolated_points[i]); + const double yaw = + tier4_autoware_utils::calcAzimuthAngle(interpolated_points[i], interpolated_points[i + 1]); const double delta_yaw = yaw - prev_yaw; - const double norm_delta_yaw = util::normalizeRadian(delta_yaw); - float clearance_from_object = std::numeric_limits::max(); - const auto image_point = util::transformMapToOptionalImage(interpolated_points[i], map_info); - if (image_point) { - clearance_from_object = only_objects_clearance_map.ptr(static_cast( - image_point.get().y))[static_cast(image_point.get().x)] * - map_info.resolution; - } - if ( - std::fabs(norm_delta_yaw) > traj_param_.delta_yaw_threshold_for_straight || - clearance_from_object < constrain_param_.clearance_from_object_for_straight) { + const double norm_delta_yaw = tier4_autoware_utils::normalizeRadian(delta_yaw); + if (std::fabs(norm_delta_yaw) > traj_param_.delta_yaw_threshold_for_straight) { break; } straight_line_idx = i; prev_yaw = yaw; } else if (i == farthest_point_idx && farthest_point_idx >= 1) { - const double yaw = util::getYawFromPoints(interpolated_points[i], interpolated_points[i - 1]); + const double yaw = + tier4_autoware_utils::calcAzimuthAngle(interpolated_points[i - 1], interpolated_points[i]); prev_yaw = yaw; } } @@ -1297,7 +495,7 @@ int EBPathOptimizer::getStraightLineIdx( return straight_line_idx; } -Constrain EBPathOptimizer::getConstrainFromConstrainRectangle( +EBPathOptimizer::Constrain EBPathOptimizer::getConstrainFromConstrainRectangle( const geometry_msgs::msg::Point & interpolated_point, const ConstrainRectangle & constrain_range) { Constrain constrain; @@ -1353,7 +551,7 @@ Constrain EBPathOptimizer::getConstrainFromConstrainRectangle( return constrain; } -ConstrainLines EBPathOptimizer::getConstrainLines( +EBPathOptimizer::ConstrainLines EBPathOptimizer::getConstrainLines( const double dx, const double dy, const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Point & opposite_point) { @@ -1381,213 +579,35 @@ ConstrainRectangle EBPathOptimizer::getConstrainRectangle( geometry_msgs::msg::Point top_left; top_left.x = clearance; top_left.y = clearance; - constrain_range.top_left = util::transformToAbsoluteCoordinate2D(top_left, anchor.pose); + constrain_range.top_left = geometry_utils::transformToAbsoluteCoordinate2D(top_left, anchor.pose); geometry_msgs::msg::Point top_right; top_right.x = clearance; top_right.y = -1 * clearance; - constrain_range.top_right = util::transformToAbsoluteCoordinate2D(top_right, anchor.pose); + constrain_range.top_right = + geometry_utils::transformToAbsoluteCoordinate2D(top_right, anchor.pose); geometry_msgs::msg::Point bottom_left; bottom_left.x = -1 * clearance; bottom_left.y = clearance; - constrain_range.bottom_left = util::transformToAbsoluteCoordinate2D(bottom_left, anchor.pose); + constrain_range.bottom_left = + geometry_utils::transformToAbsoluteCoordinate2D(bottom_left, anchor.pose); geometry_msgs::msg::Point bottom_right; bottom_right.x = -1 * clearance; bottom_right.y = -1 * clearance; - constrain_range.bottom_right = util::transformToAbsoluteCoordinate2D(bottom_right, anchor.pose); + constrain_range.bottom_right = + geometry_utils::transformToAbsoluteCoordinate2D(bottom_right, anchor.pose); constrain_range.velocity = anchor.velocity; return constrain_range; } -ConstrainRectangle EBPathOptimizer::getUpdatedConstrainRectangle( - const ConstrainRectangle & rectangle, const geometry_msgs::msg::Point & candidate_point, - const nav_msgs::msg::MapMetaData & map_info, const cv::Mat & only_objects_clearance_map) const -{ - auto rect = rectangle; - if (isClose2Object( - candidate_point, map_info, only_objects_clearance_map, - constrain_param_.min_object_clearance_for_deceleration)) { - rect.velocity = std::fmin(rect.velocity, traj_param_.max_avoiding_ego_velocity_ms); - } - if (isClose2Object( - candidate_point, map_info, only_objects_clearance_map, - constrain_param_.min_object_clearance_for_joint)) { - rect.is_including_only_smooth_range = false; - } - return rect; -} - -ConstrainRectangle EBPathOptimizer::getConstrainRectangle( - const std::vector & path_points, - const Anchor & anchor, const cv::Mat & clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const -{ - const int default_idx = -1; - const auto interpolated_path_points = - util::getInterpolatedPoints(path_points, traj_param_.delta_arc_length_for_trajectory); - const int nearest_idx = util::getNearestIdx( - interpolated_path_points, anchor.pose, default_idx, - traj_param_.delta_yaw_threshold_for_closest_point); - - float clearance = std::numeric_limits::lowest(); - geometry_msgs::msg::Point image_point; - if (util::transformMapToImage(interpolated_path_points[nearest_idx], map_info, image_point)) { - clearance = - clearance_map.ptr(static_cast(image_point.y))[static_cast(image_point.x)] * - map_info.resolution; - } - const double dist = - util::calculate2DDistance(anchor.pose.position, interpolated_path_points[nearest_idx]); - ConstrainRectangle constrain_rectangle; - // idx is valid && anchor is close to nearest path point - if (nearest_idx > default_idx && dist < clearance) { - constrain_rectangle = - getConstrainRectangle(anchor, nearest_idx, interpolated_path_points, clearance_map, map_info); - } else { - constrain_rectangle = getConstrainRectangle(anchor, constrain_param_.clearance_for_joint); - } - return constrain_rectangle; -} - -ConstrainRectangle EBPathOptimizer::getConstrainRectangle( - const Anchor & anchor, const int & nearest_idx, - const std::vector & interpolated_points, const cv::Mat & clearance_map, - const nav_msgs::msg::MapMetaData & map_info) const -{ - Anchor replaced_anchor = anchor; - replaced_anchor.pose.position = interpolated_points[nearest_idx]; - if (nearest_idx > 0) { - replaced_anchor.pose.orientation = util::getQuaternionFromPoints( - interpolated_points[nearest_idx], interpolated_points[nearest_idx - 1]); - } else if (nearest_idx == 0 && interpolated_points.size() > 1) { - replaced_anchor.pose.orientation = util::getQuaternionFromPoints( - interpolated_points[nearest_idx + 1], interpolated_points[nearest_idx]); - } - ConstrainRectangles rectangles = - getConstrainRectangles(replaced_anchor, clearance_map, clearance_map, map_info); - const double rel_plus_y = util::calculate2DDistance( - rectangles.road_constrain_rectangle.top_left, replaced_anchor.pose.position); - const double rel_minus_y = util::calculate2DDistance( - rectangles.road_constrain_rectangle.top_right, replaced_anchor.pose.position); - - ConstrainRectangle constrain_rectangle; - geometry_msgs::msg::Point top_left; - top_left.x = constrain_param_.clearance_for_joint; - top_left.y = rel_plus_y; - constrain_rectangle.top_left = - util::transformToAbsoluteCoordinate2D(top_left, replaced_anchor.pose); - geometry_msgs::msg::Point top_right; - top_right.x = constrain_param_.clearance_for_joint; - top_right.y = -1 * rel_minus_y; - constrain_rectangle.top_right = - util::transformToAbsoluteCoordinate2D(top_right, replaced_anchor.pose); - geometry_msgs::msg::Point bottom_left; - bottom_left.x = -1 * constrain_param_.clearance_for_joint; - bottom_left.y = rel_plus_y; - constrain_rectangle.bottom_left = - util::transformToAbsoluteCoordinate2D(bottom_left, replaced_anchor.pose); - geometry_msgs::msg::Point bottom_right; - bottom_right.x = -1 * constrain_param_.clearance_for_joint; - bottom_right.y = -1 * rel_minus_y; - constrain_rectangle.bottom_right = - util::transformToAbsoluteCoordinate2D(bottom_right, replaced_anchor.pose); - constrain_rectangle.velocity = anchor.velocity; - return constrain_rectangle; -} - -ConstrainRectangle EBPathOptimizer::getConstrainRectangle( - const std::vector> & occupancy_points, - const util::Rectangle & util_rect, const Anchor & anchor) const -{ - ConstrainRectangle constrain_rectangle; - constrain_rectangle.bottom_left = occupancy_points[util_rect.min_x_idx][util_rect.max_y_idx]; - constrain_rectangle.bottom_right = occupancy_points[util_rect.min_x_idx][util_rect.min_y_idx]; - constrain_rectangle.top_left = occupancy_points[util_rect.max_x_idx][util_rect.max_y_idx]; - constrain_rectangle.top_right = occupancy_points[util_rect.max_x_idx][util_rect.min_y_idx]; - - geometry_msgs::msg::Pose left_pose = anchor.pose; - left_pose.position = constrain_rectangle.top_left; - geometry_msgs::msg::Point top_left; - top_left.x = - std::fmin(keep_space_shape_ptr_->x, constrain_param_.max_lon_space_for_driveable_constraint); - top_left.y = 0; - constrain_rectangle.top_left = util::transformToAbsoluteCoordinate2D(top_left, left_pose); - geometry_msgs::msg::Point bottom_left; - bottom_left.x = 0; - bottom_left.y = 0; - constrain_rectangle.bottom_left = util::transformToAbsoluteCoordinate2D(bottom_left, left_pose); - geometry_msgs::msg::Pose right_pose = anchor.pose; - right_pose.position = constrain_rectangle.top_right; - geometry_msgs::msg::Point top_right; - top_right.x = - std::fmin(keep_space_shape_ptr_->x, constrain_param_.max_lon_space_for_driveable_constraint); - top_right.y = 0; - constrain_rectangle.top_right = util::transformToAbsoluteCoordinate2D(top_right, right_pose); - geometry_msgs::msg::Point bottom_right; - bottom_right.x = 0; - bottom_right.y = 0; - constrain_rectangle.bottom_right = - util::transformToAbsoluteCoordinate2D(bottom_right, right_pose); - constrain_rectangle.velocity = anchor.velocity; - return constrain_rectangle; -} - ConstrainRectangle EBPathOptimizer::getConstrainRectangle( - const std::vector> & occupancy_map, - const std::vector> & occupancy_points, - const Anchor & anchor, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & only_objects_clearance_map) const -{ - util::Rectangle util_rect = util::getLargestRectangle(occupancy_map); - - ConstrainRectangle constrain_rectangle; - if (util_rect.area < epsilon_) { - constrain_rectangle = getConstrainRectangle(anchor, constrain_param_.clearance_for_joint); - constrain_rectangle.is_empty_driveable_area = true; - } else { - constrain_rectangle = getConstrainRectangle(occupancy_points, util_rect, anchor); - } - geometry_msgs::msg::Point max_abs_y = occupancy_points[util_rect.max_x_idx][util_rect.max_y_idx]; - geometry_msgs::msg::Point min_abs_y = occupancy_points[util_rect.max_x_idx][util_rect.min_y_idx]; - geometry_msgs::msg::Point max_rel_y = - util::transformToRelativeCoordinate2D(max_abs_y, anchor.pose); - geometry_msgs::msg::Point min_rel_y = - util::transformToRelativeCoordinate2D(min_abs_y, anchor.pose); - if ( - (max_rel_y.y < -1 * constrain_param_.clearance_for_only_smoothing || - min_rel_y.y > constrain_param_.clearance_for_only_smoothing) && - isClose2Object( - anchor.pose.position, map_info, only_objects_clearance_map, - constrain_param_.clearance_from_object)) { - constrain_rectangle.is_including_only_smooth_range = false; - } - return constrain_rectangle; -} - -bool EBPathOptimizer::isFixingPathPoint( - [[maybe_unused]] const std::vector & path_points) - const -{ - return false; -} - -FOAData EBPathOptimizer::getFOAData( - const std::vector & rectangles, - const std::vector & interpolated_points, const int farthest_idx) -{ - FOAData foa_data; - for (const auto & rect : rectangles) { - if (rect.is_empty_driveable_area) { - foa_data.is_avoidance_possible = false; - } - } - if (!foa_data.is_avoidance_possible) { - RCLCPP_WARN( - rclcpp::get_logger("EBPathOptimizer"), - "[ObstacleAvoidancePlanner] Fail to make new trajectory from empty drivable area"); - } - - foa_data.constrain_rectangles = rectangles; - foa_data.avoiding_traj_points = calculateTrajectory( - interpolated_points, foa_data.constrain_rectangles, farthest_idx, OptMode::Visualizing); - return foa_data; + const Anchor & anchor, const double min_x, const double max_x, const double min_y, + const double max_y) const +{ + ConstrainRectangle rect; + rect.top_left = tier4_autoware_utils::calcOffsetPose(anchor.pose, max_x, max_y, 0.0).position; + rect.top_right = tier4_autoware_utils::calcOffsetPose(anchor.pose, max_x, min_y, 0.0).position; + rect.bottom_left = tier4_autoware_utils::calcOffsetPose(anchor.pose, min_x, max_y, 0.0).position; + rect.bottom_right = tier4_autoware_utils::calcOffsetPose(anchor.pose, min_x, min_y, 0.0).position; + rect.velocity = anchor.velocity; + return rect; } diff --git a/planning/obstacle_avoidance_planner/src/mpt_optimizer.cpp b/planning/obstacle_avoidance_planner/src/mpt_optimizer.cpp index 28620c3ce5f51..4427cbaf2e437 100644 --- a/planning/obstacle_avoidance_planner/src/mpt_optimizer.cpp +++ b/planning/obstacle_avoidance_planner/src/mpt_optimizer.cpp @@ -14,879 +14,875 @@ #include "obstacle_avoidance_planner/mpt_optimizer.hpp" -#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/process_cv.hpp" -#include "obstacle_avoidance_planner/util.hpp" +#include "interpolation/spline_interpolation_points_2d.hpp" +#include "obstacle_avoidance_planner/utils.hpp" #include "obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp" -#include "obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics_no_delay.hpp" +#include "tf2/utils.h" -#include -#include +#include "nav_msgs/msg/map_meta_data.hpp" -#include - -#include - -#include +#include "boost/optional.hpp" #include #include #include #include +#include #include +namespace +{ +geometry_msgs::msg::Pose convertRefPointsToPose(const ReferencePoint & ref_point) +{ + geometry_msgs::msg::Pose pose; + pose.position = ref_point.p; + pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(ref_point.yaw); + return pose; +} + +std::tuple extractBounds( + const std::vector & ref_points, const size_t l_idx) +{ + Eigen::VectorXd ub_vec(ref_points.size()); + Eigen::VectorXd lb_vec(ref_points.size()); + for (size_t i = 0; i < ref_points.size(); ++i) { + ub_vec(i) = ref_points.at(i).vehicle_bounds.at(l_idx).upper_bound; + lb_vec(i) = ref_points.at(i).vehicle_bounds.at(l_idx).lower_bound; + } + return {ub_vec, lb_vec}; +} + +Bounds findWidestBounds(const BoundsCandidates & front_bounds_candidates) +{ + double max_width = std::numeric_limits::min(); + size_t max_width_index = 0; + if (front_bounds_candidates.size() != 1) { + for (size_t candidate_idx = 0; candidate_idx < front_bounds_candidates.size(); + ++candidate_idx) { + const auto & front_bounds_candidate = front_bounds_candidates.at(candidate_idx); + const double bound_width = + front_bounds_candidate.upper_bound - front_bounds_candidate.lower_bound; + if (max_width < bound_width) { + max_width_index = candidate_idx; + max_width = bound_width; + } + } + } + return front_bounds_candidates.at(max_width_index); +} + +double calcOverlappedBounds( + const geometry_msgs::msg::Pose & front_point, const Bounds & front_bounds_candidate, + const geometry_msgs::msg::Pose & prev_front_point, const Bounds & prev_front_continuous_bounds) +{ + const double avoiding_yaw = + tier4_autoware_utils::normalizeRadian(tf2::getYaw(front_point.orientation) + M_PI_2); + + geometry_msgs::msg::Point ub_pos; + ub_pos.x = front_point.position.x + front_bounds_candidate.upper_bound * std::cos(avoiding_yaw); + ub_pos.y = front_point.position.y + front_bounds_candidate.upper_bound * std::sin(avoiding_yaw); + + geometry_msgs::msg::Point lb_pos; + lb_pos.x = front_point.position.x + front_bounds_candidate.lower_bound * std::cos(avoiding_yaw); + lb_pos.y = front_point.position.y + front_bounds_candidate.lower_bound * std::sin(avoiding_yaw); + + const double projected_ub_y = + geometry_utils::transformToRelativeCoordinate2D(ub_pos, prev_front_point).y; + const double projected_lb_y = + geometry_utils::transformToRelativeCoordinate2D(lb_pos, prev_front_point).y; + + const double min_ub = std::min(projected_ub_y, prev_front_continuous_bounds.upper_bound); + const double max_lb = std::max(projected_lb_y, prev_front_continuous_bounds.lower_bound); + + const double overlapped_length = min_ub - max_lb; + return overlapped_length; +} + +geometry_msgs::msg::Pose calcVehiclePose( + const ReferencePoint & ref_point, const double lat_error, const double yaw_error, + const double offset) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = ref_point.p.x - std::sin(ref_point.yaw) * lat_error - + std::cos(ref_point.yaw + yaw_error) * offset; + pose.position.y = ref_point.p.y + std::cos(ref_point.yaw) * lat_error - + std::sin(ref_point.yaw + yaw_error) * offset; + pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(ref_point.yaw + yaw_error); + + return pose; +} + +template +void trimPoints(std::vector & points) +{ + std::vector trimmed_points; + constexpr double epsilon = 1e-6; + + auto itr = points.begin(); + while (itr != points.end() - 1) { + bool is_overlapping = false; + if (itr != points.begin()) { + const auto & p_front = tier4_autoware_utils::getPoint(*itr); + const auto & p_back = tier4_autoware_utils::getPoint(*(itr + 1)); + + const double dx = p_front.x - p_back.x; + const double dy = p_front.y - p_back.y; + if (dx * dx + dy * dy < epsilon) { + is_overlapping = true; + } + } + if (is_overlapping) { + itr = points.erase(itr); + } else { + ++itr; + } + } +} + +std::vector eigenVectorToStdVector(const Eigen::VectorXd & eigen_vec) +{ + return {eigen_vec.data(), eigen_vec.data() + eigen_vec.rows()}; +} + +double calcLateralError( + const geometry_msgs::msg::Point & target_point, const geometry_msgs::msg::Pose ref_pose) +{ + const double err_x = target_point.x - ref_pose.position.x; + const double err_y = target_point.y - ref_pose.position.y; + const double ref_yaw = tf2::getYaw(ref_pose.orientation); + const double lat_err = -std::sin(ref_yaw) * err_x + std::cos(ref_yaw) * err_y; + return lat_err; +} + +Eigen::Vector2d getState( + const geometry_msgs::msg::Pose & target_pose, const geometry_msgs::msg::Pose & ref_pose) +{ + const double lat_error = calcLateralError(target_pose.position, ref_pose); + const double yaw_error = tier4_autoware_utils::normalizeRadian( + tf2::getYaw(target_pose.orientation) - tf2::getYaw(ref_pose.orientation)); + Eigen::VectorXd kinematics = Eigen::VectorXd::Zero(2); + kinematics << lat_error, yaw_error; + return kinematics; +} + +std::vector slerpYawFromReferencePoints(const std::vector & ref_points) +{ + if (ref_points.size() == 1) { + return {ref_points.front().yaw}; + } + + std::vector points; + for (const auto & ref_point : ref_points) { + points.push_back(ref_point.p); + } + return interpolation::slerpYawFromPoints(points); +} +} // namespace + MPTOptimizer::MPTOptimizer( - const bool is_showing_debug_info, const QPParam & qp_param, const TrajectoryParam & traj_param, - const ConstrainParam & constraint_param, const VehicleParam & vehicle_param, - const MPTParam & mpt_param) -: is_showing_debug_info_(is_showing_debug_info) + const bool is_showing_debug_info, const TrajectoryParam & traj_param, + const VehicleParam & vehicle_param, const MPTParam & mpt_param) +: is_showing_debug_info_(is_showing_debug_info), + traj_param_(traj_param), + vehicle_param_(vehicle_param), + mpt_param_(mpt_param), + vehicle_model_ptr_( + std::make_unique(vehicle_param_.wheelbase, mpt_param_.max_steer_rad)), + osqp_solver_ptr_(std::make_unique(osqp_epsilon_)) { - qp_param_ptr_ = std::make_unique(qp_param); - traj_param_ptr_ = std::make_unique(traj_param); - constraint_param_ptr_ = std::make_unique(constraint_param); - vehicle_param_ptr_ = std::make_unique(vehicle_param); - mpt_param_ptr_ = std::make_unique(mpt_param); - - vehicle_model_ptr_ = std::make_unique( - vehicle_param_ptr_->wheelbase, vehicle_param_ptr_->max_steer_rad, - vehicle_param_ptr_->steer_tau); } -boost::optional MPTOptimizer::getModelPredictiveTrajectory( +boost::optional MPTOptimizer::getModelPredictiveTrajectory( const bool enable_avoidance, const std::vector & smoothed_points, const std::vector & path_points, const std::unique_ptr & prev_trajs, const CVMaps & maps, - const geometry_msgs::msg::Pose & ego_pose, DebugData * debug_data) + const geometry_msgs::msg::Pose & current_ego_pose, const double current_ego_vel, + std::shared_ptr debug_data_ptr) { - auto t_start1 = std::chrono::high_resolution_clock::now(); + stop_watch_.tic(__func__); + + current_ego_pose_ = current_ego_pose; + current_ego_vel_ = current_ego_vel; + if (smoothed_points.empty()) { RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "return boost::none since smoothed_points is empty"); return boost::none; } - geometry_msgs::msg::Pose origin_pose = smoothed_points.front().pose; - if (prev_trajs) { - const int prev_nearest_idx = util::getNearestIdx( - prev_trajs->model_predictive_trajectory, smoothed_points.front().pose.position); - origin_pose = prev_trajs->model_predictive_trajectory.at(prev_nearest_idx).pose; - } - std::vector ref_points = getReferencePoints( - origin_pose, ego_pose, smoothed_points, path_points, prev_trajs, maps, debug_data); - if (ref_points.empty()) { + std::vector full_ref_points = + getReferencePoints(smoothed_points, prev_trajs, enable_avoidance, maps, debug_data_ptr); + if (full_ref_points.empty()) { RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "return boost::none since ref_points is empty"); return boost::none; - } - - const auto mpt_matrix = generateMPTMatrix(ref_points, path_points, prev_trajs); - if (!mpt_matrix) { + } else if (full_ref_points.size() == 1) { RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, - "return boost::none since matrix has nan"); + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, + "return boost::none since ref_points.size() == 1"); return boost::none; } + debug_data_ptr->mpt_fixed_traj = getMPTFixedPoints(full_ref_points); + + std::vector fixed_ref_points; + std::vector non_fixed_ref_points; + bool is_fixing_ref_points = true; + for (size_t i = 0; i < full_ref_points.size(); ++i) { + if (i == full_ref_points.size() - 1) { + if (!full_ref_points.at(i).fix_kinematic_state) { + is_fixing_ref_points = false; + } + } else if ( + // fix first three points + full_ref_points.at(i).fix_kinematic_state && full_ref_points.at(i + 1).fix_kinematic_state && + (i + 2 < full_ref_points.size() && full_ref_points.at(i + 2).fix_kinematic_state) && + (i + 3 < full_ref_points.size() && full_ref_points.at(i + 3).fix_kinematic_state)) { + } else { + is_fixing_ref_points = false; + } + + if (is_fixing_ref_points) { + fixed_ref_points.push_back(full_ref_points.at(i)); + } else { + non_fixed_ref_points.push_back(full_ref_points.at(i)); + } + } + + // calculate B and W matrices + const auto mpt_matrix = generateMPTMatrix(non_fixed_ref_points, debug_data_ptr); + + // calculate Q and R matrices + const auto val_matrix = generateValueMatrix(non_fixed_ref_points, path_points, debug_data_ptr); + const auto optimized_control_variables = executeOptimization( - enable_avoidance, mpt_matrix.get(), ref_points, path_points, maps, debug_data); + prev_trajs, enable_avoidance, mpt_matrix, val_matrix, non_fixed_ref_points, debug_data_ptr); if (!optimized_control_variables) { RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "return boost::none since could not solve qp"); return boost::none; } - const auto mpt_points = - getMPTPoints(ref_points, optimized_control_variables.get(), mpt_matrix.get(), smoothed_points); + const auto mpt_points = getMPTPoints( + fixed_ref_points, non_fixed_ref_points, optimized_control_variables.get(), mpt_matrix, + debug_data_ptr); + + auto full_optimized_ref_points = fixed_ref_points; + full_optimized_ref_points.insert( + full_optimized_ref_points.end(), non_fixed_ref_points.begin(), non_fixed_ref_points.end()); + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; - auto t_end1 = std::chrono::high_resolution_clock::now(); - float elapsed_ms1 = std::chrono::duration(t_end1 - t_start1).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, "MPT time: = %f [ms]", elapsed_ms1); MPTTrajs mpt_trajs; mpt_trajs.mpt = mpt_points; - mpt_trajs.ref_points = ref_points; + mpt_trajs.ref_points = full_optimized_ref_points; return mpt_trajs; } std::vector MPTOptimizer::getReferencePoints( - const geometry_msgs::msg::Pose & origin_pose, const geometry_msgs::msg::Pose & ego_pose, - const std::vector & points, - const std::vector & path_points, - const std::unique_ptr & prev_trajs, const CVMaps & maps, - DebugData * debug_data) const + const std::vector & smoothed_points, + const std::unique_ptr & prev_trajs, const bool enable_avoidance, + const CVMaps & maps, std::shared_ptr debug_data_ptr) const { - const auto ref_points = - convertToReferencePoints(points, path_points, prev_trajs, ego_pose, maps, debug_data); - - const int begin_idx = util::getNearestPointIdx(ref_points, origin_pose.position); - const auto first_it = ref_points.begin() + begin_idx; - int num_points = std::min( - static_cast(ref_points.size()) - 1 - begin_idx, traj_param_ptr_->num_sampling_points); - num_points = std::max(num_points, 0); - std::vector truncated_points(first_it, first_it + num_points); - calcInitialState(&truncated_points, origin_pose); - return truncated_points; -} + stop_watch_.tic(__func__); + + const auto ref_points = [&]() { + auto ref_points = [&]() { + // TODO(murooka) consider better algorithm to calculate fixed/non-fixed reference points + const auto fixed_ref_points = getFixedReferencePoints(smoothed_points, prev_trajs); + + // if no fixed_ref_points + if (fixed_ref_points.empty()) { + // interpolate and crop backward + const auto interpolated_points = interpolation_utils::getInterpolatedPoints( + smoothed_points, mpt_param_.delta_arc_length_for_mpt_points); + const auto cropped_interpolated_points = points_utils::clipBackwardPoints( + interpolated_points, current_ego_pose_.position, traj_param_.backward_fixing_distance, + mpt_param_.delta_arc_length_for_mpt_points); + + return points_utils::convertToReferencePoints(cropped_interpolated_points); + } -std::vector MPTOptimizer::convertToReferencePoints( - const std::vector & points, - [[maybe_unused]] const std::vector & path_points, - const std::unique_ptr & prev_trajs, - [[maybe_unused]] const geometry_msgs::msg::Pose & ego_pose, [[maybe_unused]] const CVMaps & maps, - DebugData * debug_data) const -{ - const auto interpolated_points = - util::getInterpolatedPoints(points, traj_param_ptr_->delta_arc_length_for_mpt_points); - if (interpolated_points.empty()) { + // calc non fixed traj points + const size_t seg_idx = + tier4_autoware_utils::findNearestSegmentIndex(smoothed_points, fixed_ref_points.back().p); + const auto non_fixed_traj_points = + std::vector{ + smoothed_points.begin() + seg_idx, smoothed_points.end()}; + + const double offset = tier4_autoware_utils::calcLongitudinalOffsetToSegment( + non_fixed_traj_points, 0, fixed_ref_points.back().p) + + mpt_param_.delta_arc_length_for_mpt_points; + const auto non_fixed_interpolated_traj_points = interpolation_utils::getInterpolatedPoints( + non_fixed_traj_points, mpt_param_.delta_arc_length_for_mpt_points, offset); + const auto non_fixed_ref_points = + points_utils::convertToReferencePoints(non_fixed_interpolated_traj_points); + + // make ref points + auto ref_points = fixed_ref_points; + ref_points.insert(ref_points.end(), non_fixed_ref_points.begin(), non_fixed_ref_points.end()); + + return ref_points; + }(); + + if (ref_points.empty()) { + return std::vector{}; + } + + // set some information to reference points considering fix kinematics + trimPoints(ref_points); + calcOrientation(ref_points); + calcVelocity(ref_points, smoothed_points); + calcCurvature(ref_points); + calcArcLength(ref_points); + calcPlanningFromEgo( + ref_points); // NOTE: fix_kinematic_state will be updated when planning from ego + + // crop trajectory with margin to calculate vehicle bounds at the end point + constexpr double tmp_ref_points_margin = 20.0; + const double ref_length_with_margin = + traj_param_.num_sampling_points * mpt_param_.delta_arc_length_for_mpt_points + + tmp_ref_points_margin; + ref_points = points_utils::clipForwardPoints(ref_points, 0, ref_length_with_margin); + + // set bounds information + calcBounds(ref_points, enable_avoidance, maps, debug_data_ptr); + calcVehicleBounds(ref_points, maps, debug_data_ptr, enable_avoidance); + + // set extra information (alpha and has_object_collision) + // NOTE: This must be after bounds calculation. + calcExtraPoints(ref_points, prev_trajs); + + const double ref_length = + traj_param_.num_sampling_points * mpt_param_.delta_arc_length_for_mpt_points; + ref_points = points_utils::clipForwardPoints(ref_points, 0, ref_length); + + // bounds information is assigned to debug data after truncating reference points + debug_data_ptr->ref_points = ref_points; + + return ref_points; + }(); + if (ref_points.empty()) { return std::vector{}; } - auto reference_points = getBaseReferencePoints(interpolated_points, prev_trajs, debug_data); - - calcOrientation(&reference_points); - calcVelocity(&reference_points, points); - calcCurvature(&reference_points); - calcArcLength(&reference_points); - calcExtraPoints(&reference_points); - return reference_points; + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return ref_points; } -void MPTOptimizer::calcOrientation(std::vector * ref_points) const +void MPTOptimizer::calcPlanningFromEgo(std::vector & ref_points) const { - if (!ref_points) { - return; - } - for (std::size_t i = 0; i < ref_points->size(); i++) { - if (ref_points->at(i).fix_state) { - continue; + // if plan from ego + constexpr double epsilon = 1e-04; + const bool plan_from_ego = + mpt_param_.plan_from_ego && std::abs(current_ego_vel_) < epsilon && ref_points.size() > 1; + if (plan_from_ego) { + for (auto & ref_point : ref_points) { + ref_point.fix_kinematic_state = boost::none; } - if (i > 0) { - ref_points->at(i).q = - util::getQuaternionFromPoints(ref_points->at(i).p, ref_points->at(i - 1).p); - } else if (i == 0 && ref_points->size() > 1) { - ref_points->at(i).q = - util::getQuaternionFromPoints(ref_points->at(i + 1).p, ref_points->at(i).p); - } - ref_points->at(i).yaw = tf2::getYaw(ref_points->at(i).q); + /* + // interpolate and crop backward + const auto interpolated_points = interpolation_utils::getInterpolatedPoints( + points, + mpt_param_.delta_arc_length_for_mpt_points); const auto cropped_interpolated_points = + points_utils::clipBackwardPoints( interpolated_points, current_pose_.position, + traj_param_.backward_fixing_distance, mpt_param_.delta_arc_length_for_mpt_points); + + auto cropped_ref_points = + points_utils::convertToReferencePoints(cropped_interpolated_points); + */ + + // assign fix kinematics + const size_t nearest_ref_idx = + tier4_autoware_utils::findNearestIndex(ref_points, current_ego_pose_.position); + + // calculate cropped_ref_points.at(nearest_ref_idx) with yaw + const geometry_msgs::msg::Pose nearest_ref_pose = [&]() -> geometry_msgs::msg::Pose { + geometry_msgs::msg::Pose pose; + pose.position = ref_points.at(nearest_ref_idx).p; + + const size_t prev_nearest_ref_idx = + (nearest_ref_idx == ref_points.size() - 1) ? nearest_ref_idx - 1 : nearest_ref_idx; + pose.orientation = geometry_utils::getQuaternionFromPoints( + ref_points.at(prev_nearest_ref_idx + 1).p, ref_points.at(prev_nearest_ref_idx).p); + return pose; + }(); + + ref_points.at(nearest_ref_idx).fix_kinematic_state = + getState(current_ego_pose_, nearest_ref_pose); + ref_points.at(nearest_ref_idx).plan_from_ego = true; } } -void MPTOptimizer::calcVelocity( - std::vector * ref_points, - const std::vector & points) const +std::vector MPTOptimizer::getFixedReferencePoints( + const std::vector & points, + const std::unique_ptr & prev_trajs) const { - if (!ref_points) { - return; - } - for (std::size_t i = 0; i < ref_points->size(); i++) { - ref_points->at(i).v = - points[util::getNearestIdx(points, ref_points->at(i).p)].longitudinal_velocity_mps; + if ( + !prev_trajs || prev_trajs->model_predictive_trajectory.empty() || + prev_trajs->mpt_ref_points.empty() || + prev_trajs->model_predictive_trajectory.size() != prev_trajs->mpt_ref_points.size()) { + return std::vector(); } -} -void MPTOptimizer::calcCurvature(std::vector * ref_points) const -{ - if (!ref_points) { - return; + if (!mpt_param_.fix_points_around_ego) { + return std::vector(); } - int num_points = static_cast(ref_points->size()); - - /* calculate curvature by circle fitting from three points */ - geometry_msgs::msg::Point p1, p2, p3; - int max_smoothing_num = static_cast(std::floor(0.5 * (num_points - 1))); - int L = std::min(mpt_param_ptr_->num_curvature_sampling_points, max_smoothing_num); - for (int i = L; i < num_points - L; ++i) { - p1 = ref_points->at(i - L).p; - p2 = ref_points->at(i).p; - p3 = ref_points->at(i + L).p; - double den = std::max( - util::calculate2DDistance(p1, p2) * util::calculate2DDistance(p2, p3) * - util::calculate2DDistance(p3, p1), - 0.0001); - const double curvature = - 2.0 * ((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) / den; - if (!ref_points->at(i).fix_state) { - ref_points->at(i).k = curvature; + const auto & prev_ref_points = prev_trajs->mpt_ref_points; + const int nearest_prev_ref_idx = + tier4_autoware_utils::findNearestIndex(prev_ref_points, current_ego_pose_.position); + + // calculate begin_prev_ref_idx + const int begin_prev_ref_idx = [&]() { + const int backward_fixing_num = + traj_param_.backward_fixing_distance / mpt_param_.delta_arc_length_for_mpt_points; + + return std::max(0, nearest_prev_ref_idx - backward_fixing_num); + }(); + + // calculate end_prev_ref_idx + const int end_prev_ref_idx = [&]() { + const double forward_fixed_length = std::max( + current_ego_vel_ * traj_param_.forward_fixing_min_time, + traj_param_.forward_fixing_min_distance); + + const int forward_fixing_num = + forward_fixed_length / mpt_param_.delta_arc_length_for_mpt_points; + return std::min( + static_cast(prev_ref_points.size()) - 1, nearest_prev_ref_idx + forward_fixing_num); + }(); + + bool points_reaching_prev_points_flag = false; + std::vector fixed_ref_points; + for (size_t i = begin_prev_ref_idx; i <= static_cast(end_prev_ref_idx); ++i) { + const auto & prev_ref_point = prev_ref_points.at(i); + + if (!points_reaching_prev_points_flag) { + if (tier4_autoware_utils::calcSignedArcLength(points, 0, prev_ref_point.p) < 0) { + continue; + } + points_reaching_prev_points_flag = true; } - } - /* first and last curvature is copied from next value */ - for (int i = 0; i < std::min(L, num_points); ++i) { - if (!ref_points->at(i).fix_state) { - ref_points->at(i).k = ref_points->at(std::min(L, num_points - 1)).k; - } - if (!ref_points->at(num_points - i - 1).fix_state) { - ref_points->at(num_points - i - 1).k = ref_points->at(std::max(num_points - L - 1, 0)).k; - } + ReferencePoint fixed_ref_point; + fixed_ref_point = prev_ref_point; + fixed_ref_point.fix_kinematic_state = prev_ref_point.optimized_kinematic_state; + + fixed_ref_points.push_back(fixed_ref_point); } + + return fixed_ref_points; } -void MPTOptimizer::calcArcLength(std::vector * ref_points) const +std::vector MPTOptimizer::getMPTFixedPoints( + const std::vector & ref_points) const { - if (!ref_points) { - return; - } - for (std::size_t i = 0; i < ref_points->size(); i++) { - if (i > 0) { - geometry_msgs::msg::Point a, b; - a = ref_points->at(i).p; - b = ref_points->at(i - 1).p; - ref_points->at(i).s = ref_points->at(i - 1).s + util::calculate2DDistance(a, b); + std::vector mpt_fixed_traj; + for (size_t i = 0; i < ref_points.size(); ++i) { + const auto & ref_point = ref_points.at(i); + + if (ref_point.fix_kinematic_state) { + const double lat_error = ref_point.fix_kinematic_state.get()(0); + const double yaw_error = ref_point.fix_kinematic_state.get()(1); + + autoware_auto_planning_msgs::msg::TrajectoryPoint fixed_traj_point; + fixed_traj_point.pose = calcVehiclePose(ref_point, lat_error, yaw_error, 0.0); + mpt_fixed_traj.push_back(fixed_traj_point); } } + + return mpt_fixed_traj; } -void MPTOptimizer::calcExtraPoints(std::vector * ref_points) const +// predict equation: x = Bex u + Wex (u includes x_0) +// cost function: J = x' Qex x + u' Rex u +MPTOptimizer::MPTMatrix MPTOptimizer::generateMPTMatrix( + const std::vector & ref_points, std::shared_ptr debug_data_ptr) const { - if (!ref_points) { - return; + if (ref_points.empty()) { + return MPTMatrix{}; } - for (std::size_t i = 0; i < ref_points->size(); i++) { - const double p1_target_s = ref_points->at(i).s + mpt_param_ptr_->top_point_dist_from_base_link; - const int nearest_p1_idx = util::getNearestIdx(*ref_points, p1_target_s, i); - ref_points->at(i).top_pose.position = ref_points->at(nearest_p1_idx).p; - - const double p2_target_s = ref_points->at(i).s + mpt_param_ptr_->mid_point_dist_from_base_link; - const int nearest_p2_idx = util::getNearestIdx(*ref_points, p2_target_s, i); - ref_points->at(i).mid_pose.position = ref_points->at(nearest_p2_idx).p; - - ref_points->at(i).top_pose.orientation = - util::getQuaternionFromPoints(ref_points->at(i).top_pose.position, ref_points->at(i).p); - if (static_cast(i) == nearest_p1_idx && i > 0) { - ref_points->at(i).top_pose.orientation = - util::getQuaternionFromPoints(ref_points->at(i).p, ref_points->at(i - 1).p); - } else if (static_cast(i) == nearest_p1_idx) { - ref_points->at(i).top_pose.orientation = ref_points->at(i).q; - } - ref_points->at(i).mid_pose.orientation = ref_points->at(i).top_pose.orientation; - const double delta_yaw = - tf2::getYaw(ref_points->at(i).top_pose.orientation) - ref_points->at(i).yaw; - const double norm_delta_yaw = util::normalizeRadian(delta_yaw); - ref_points->at(i).delta_yaw_from_p1 = norm_delta_yaw; - ref_points->at(i).delta_yaw_from_p2 = ref_points->at(i).delta_yaw_from_p1; - } -} + stop_watch_.tic(__func__); -void MPTOptimizer::calcInitialState( - std::vector * ref_points, const geometry_msgs::msg::Pose & origin_pose) const -{ - if (!ref_points) { - return; - } + // NOTE: center offset of vehicle is always 0 in this algorithm. + // vehicle_model_ptr_->updateCenterOffset(0.0); - if (ref_points->empty()) { - return; - } + const size_t N_ref = ref_points.size(); + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t D_v = D_x + D_u * (N_ref - 1); - boost::optional begin_idx = boost::none; - double accum_s = 0; + Eigen::MatrixXd Bex = Eigen::MatrixXd::Zero(D_x * N_ref, D_v); + Eigen::VectorXd Wex = Eigen::VectorXd::Zero(D_x * N_ref); - for (std::size_t i = 0; i < ref_points->size(); i++) { - double ds = 0.0; - if (i < ref_points->size() - 1) { - ds = ref_points->at(i + 1).s - ref_points->at(i).s; - } else if (i == ref_points->size() - 1 && ref_points->size() > 1) { - ds = ref_points->at(i).s - ref_points->at(i - 1).s; - } - accum_s += ds; - constexpr double max_s_for_prev_point = 3; - if (accum_s < max_s_for_prev_point && ref_points->at(i).fix_state) { - begin_idx = i; - break; + Eigen::MatrixXd Ad(D_x, D_x); + Eigen::MatrixXd Bd(D_x, D_u); + Eigen::MatrixXd Wd(D_x, 1); + + // predict kinematics for N_ref times + for (size_t i = 0; i < N_ref; ++i) { + if (i == 0) { + Bex.block(0, 0, D_x, D_x) = Eigen::MatrixXd::Identity(D_x, D_x); + continue; } - } - Eigen::VectorXd initial_state; - if (begin_idx) { - *ref_points = - std::vector{ref_points->begin() + begin_idx.get(), ref_points->end()}; - ref_points->front().optimized_state = ref_points->front().fix_state.get(); - } else { - ref_points->front().optimized_state = getState(origin_pose, ref_points->front()); - } -} + const int idx_x_i = i * D_x; + const int idx_x_i_prev = (i - 1) * D_x; + const int idx_u_i_prev = (i - 1) * D_u; -/* - * predict equation: Xec = Aex * x0 + Bex * Uex + Wex - * cost function: J = Xex' * Qex * Xex + (Uex - Uref)' * R1ex * (Uex - Uref_ex) + Uex' * R2ex * Uex - * Qex = diag([Q,Q,...]), R1ex = diag([R,R,...]) - */ -boost::optional MPTOptimizer::generateMPTMatrix( - const std::vector & reference_points, - const std::vector & path_points, - const std::unique_ptr & prev_trajs) const -{ - const int N = reference_points.size(); - const int DIM_X = vehicle_model_ptr_->getDimX(); - const int DIM_U = vehicle_model_ptr_->getDimU(); - const int DIM_Y = vehicle_model_ptr_->getDimY(); - - Eigen::MatrixXd Aex = Eigen::MatrixXd::Zero(DIM_X * N, DIM_X); // state transition - Eigen::MatrixXd Bex = Eigen::MatrixXd::Zero(DIM_X * N, DIM_U * N); // control input - Eigen::MatrixXd Wex = Eigen::MatrixXd::Zero(DIM_X * N, 1); - Eigen::MatrixXd Cex = Eigen::MatrixXd::Zero(DIM_Y * N, DIM_X * N); - Eigen::MatrixXd Qex = Eigen::MatrixXd::Zero(DIM_Y * N, DIM_Y * N); - Eigen::MatrixXd R1ex = Eigen::MatrixXd::Zero(DIM_U * N, DIM_U * N); - Eigen::MatrixXd R2ex = Eigen::MatrixXd::Zero(DIM_U * N, DIM_U * N); - Eigen::MatrixXd Uref_ex = Eigen::MatrixXd::Zero(DIM_U * N, 1); - - /* weight matrix depends on the vehicle model */ - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(DIM_Y, DIM_Y); - Eigen::MatrixXd R = Eigen::MatrixXd::Zero(DIM_U, DIM_U); - Eigen::MatrixXd Q_adaptive = Eigen::MatrixXd::Zero(DIM_Y, DIM_Y); - Eigen::MatrixXd R_adaptive = Eigen::MatrixXd::Zero(DIM_U, DIM_U); - Q(0, 0) = mpt_param_ptr_->lat_error_weight; - if (!prev_trajs) { - const double initial_lat_error_weight = mpt_param_ptr_->lat_error_weight * 1000; - Q(0, 0) = initial_lat_error_weight; - } - Q(1, 1) = mpt_param_ptr_->yaw_error_weight; - R(0, 0) = mpt_param_ptr_->steer_input_weight; - - Eigen::MatrixXd Ad(DIM_X, DIM_X); - Eigen::MatrixXd Bd(DIM_X, DIM_U); - Eigen::MatrixXd Wd(DIM_X, 1); - Eigen::MatrixXd Cd(DIM_Y, DIM_X); - Eigen::MatrixXd Uref(DIM_U, 1); - - geometry_msgs::msg::Pose last_ref_pose; - last_ref_pose.position = reference_points.back().p; - last_ref_pose.orientation = reference_points.back().q; - const auto last_extended_point = util::getLastExtendedPoint( - path_points.back(), last_ref_pose, traj_param_ptr_->delta_yaw_threshold_for_closest_point, - traj_param_ptr_->max_dist_for_extending_end_point); - - /* predict dynamics for N times */ - for (int i = 0; i < N; ++i) { - const double ref_k = reference_points[i].k; - double ds = 0.0; - if (i < N - 1) { - ds = reference_points[i + 1].s - reference_points[i].s; - } else if (i == N - 1 && N > 1) { - ds = reference_points[i].s - reference_points[i - 1].s; - } + const double ds = [&]() { + if (N_ref == 1) { + return 0.0; + } + const size_t prev_idx = (i < N_ref - 1) ? i + 1 : i; + return ref_points.at(prev_idx).s - ref_points.at(prev_idx - 1).s; + }(); - /* get discrete state matrix A, B, C, W */ + // get discrete kinematics matrix A, B, W + const double ref_k = ref_points.at(std::max(0, static_cast(i) - 1)).k; vehicle_model_ptr_->setCurvature(ref_k); - vehicle_model_ptr_->calculateDiscreteMatrix(&Ad, &Bd, &Cd, &Wd, ds); - - Q_adaptive = Q; - R_adaptive = R; - if (i == N - 1 && last_extended_point) { - Q_adaptive(0, 0) = mpt_param_ptr_->terminal_path_lat_error_weight; - Q_adaptive(1, 1) = mpt_param_ptr_->terminal_path_yaw_error_weight; - } else if (i == N - 1) { - Q_adaptive(0, 0) = mpt_param_ptr_->terminal_lat_error_weight; - Q_adaptive(1, 1) = mpt_param_ptr_->terminal_yaw_error_weight; - } + vehicle_model_ptr_->calculateStateEquationMatrix(Ad, Bd, Wd, ds); - /* update mpt matrix */ - int idx_x_i = i * DIM_X; - int idx_x_i_prev = (i - 1) * DIM_X; - int idx_u_i = i * DIM_U; - int idx_y_i = i * DIM_Y; - if (i == 0) { - Aex.block(0, 0, DIM_X, DIM_X) = Ad; - Bex.block(0, 0, DIM_X, DIM_U) = Bd; - Wex.block(0, 0, DIM_X, 1) = Wd; - } else { - Aex.block(idx_x_i, 0, DIM_X, DIM_X) = Ad * Aex.block(idx_x_i_prev, 0, DIM_X, DIM_X); - for (int j = 0; j < i; ++j) { - int idx_u_j = j * DIM_U; - Bex.block(idx_x_i, idx_u_j, DIM_X, DIM_U) = - Ad * Bex.block(idx_x_i_prev, idx_u_j, DIM_X, DIM_U); - } - Wex.block(idx_x_i, 0, DIM_X, 1) = Ad * Wex.block(idx_x_i_prev, 0, DIM_X, 1) + Wd; - } - Bex.block(idx_x_i, idx_u_i, DIM_X, DIM_U) = Bd; - Cex.block(idx_y_i, idx_x_i, DIM_Y, DIM_X) = Cd; - Qex.block(idx_y_i, idx_y_i, DIM_Y, DIM_Y) = Q_adaptive; - R1ex.block(idx_u_i, idx_u_i, DIM_U, DIM_U) = R_adaptive; - - /* get reference input (feed-forward) */ - vehicle_model_ptr_->calculateReferenceInput(&Uref); - if (std::fabs(Uref(0, 0)) < mpt_param_ptr_->zero_ff_steer_angle * M_PI / 180.0) { - Uref(0, 0) = 0.0; // ignore curvature noise + Bex.block(idx_x_i, 0, D_x, D_x) = Ad * Bex.block(idx_x_i_prev, 0, D_x, D_x); + Bex.block(idx_x_i, D_x + idx_u_i_prev, D_x, D_u) = Bd; + + for (size_t j = 0; j < i - 1; ++j) { + size_t idx_u_j = j * D_u; + Bex.block(idx_x_i, D_x + idx_u_j, D_x, D_u) = + Ad * Bex.block(idx_x_i_prev, D_x + idx_u_j, D_x, D_u); } - Uref_ex.block(i * DIM_U, 0, DIM_U, 1) = Uref; - } - addSteerWeightR(&R1ex, reference_points); + Wex.segment(idx_x_i, D_x) = Ad * Wex.block(idx_x_i_prev, 0, D_x, 1) + Wd; + } MPTMatrix m; - m.Aex = Aex; m.Bex = Bex; m.Wex = Wex; - m.Cex = Cex; - m.Qex = Qex; - m.R1ex = R1ex; - m.R2ex = R2ex; - m.Uref_ex = Uref_ex; - if ( - m.Aex.array().isNaN().any() || m.Bex.array().isNaN().any() || m.Cex.array().isNaN().any() || - m.Wex.array().isNaN().any() || m.Qex.array().isNaN().any() || m.R1ex.array().isNaN().any() || - m.R2ex.array().isNaN().any() || m.Uref_ex.array().isNaN().any()) { - RCLCPP_WARN(rclcpp::get_logger("MPTOptimizer"), "[Avoidance] MPT matrix includes NaN."); - return boost::none; - } + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; return m; } -void MPTOptimizer::addSteerWeightR( - Eigen::MatrixXd * R, const std::vector & ref_points) const +MPTOptimizer::ValueMatrix MPTOptimizer::generateValueMatrix( + const std::vector & ref_points, + const std::vector & path_points, + std::shared_ptr debug_data_ptr) const { - const int N = ref_points.size(); - constexpr double DT = 0.1; - constexpr double ctrl_period = 0.03; - - /* add steering rate : weight for (u(i) - u(i-1) / dt )^2 */ - for (int i = 0; i < N - 1; ++i) { - const double steer_rate_r = mpt_param_ptr_->steer_rate_weight / (DT * DT); - (*R)(i + 0, i + 0) += steer_rate_r; - (*R)(i + 1, i + 0) -= steer_rate_r; - (*R)(i + 0, i + 1) -= steer_rate_r; - (*R)(i + 1, i + 1) += steer_rate_r; - } - if (N > 1) { - // steer rate i = 0 - (*R)(0, 0) += mpt_param_ptr_->steer_rate_weight / (ctrl_period * ctrl_period); + if (ref_points.empty()) { + return ValueMatrix{}; } - /* add steering acceleration : weight for { (u(i+1) - 2*u(i) + u(i-1)) / dt^2 }^2 */ - const double steer_acc_r = mpt_param_ptr_->steer_acc_weight / std::pow(DT, 4); - const double steer_acc_r_cp1 = mpt_param_ptr_->steer_acc_weight / (std::pow(DT, 3) * ctrl_period); - const double steer_acc_r_cp2 = - mpt_param_ptr_->steer_acc_weight / (std::pow(DT, 2) * std::pow(ctrl_period, 2)); - const double steer_acc_r_cp4 = mpt_param_ptr_->steer_acc_weight / std::pow(ctrl_period, 4); - for (int i = 1; i < N - 1; ++i) { - (*R)(i - 1, i - 1) += (steer_acc_r); - (*R)(i - 1, i + 0) += (steer_acc_r * -2.0); - (*R)(i - 1, i + 1) += (steer_acc_r); - (*R)(i + 0, i - 1) += (steer_acc_r * -2.0); - (*R)(i + 0, i + 0) += (steer_acc_r * 4.0); - (*R)(i + 0, i + 1) += (steer_acc_r * -2.0); - (*R)(i + 1, i - 1) += (steer_acc_r); - (*R)(i + 1, i + 0) += (steer_acc_r * -2.0); - (*R)(i + 1, i + 1) += (steer_acc_r); - } - if (N > 1) { - // steer acc i = 1 - (*R)(0, 0) += steer_acc_r * 1.0 + steer_acc_r_cp2 * 1.0 + steer_acc_r_cp1 * 2.0; - (*R)(1, 0) += steer_acc_r * -1.0 + steer_acc_r_cp1 * -1.0; - (*R)(0, 1) += steer_acc_r * -1.0 + steer_acc_r_cp1 * -1.0; - (*R)(1, 1) += steer_acc_r * 1.0; - // steer acc i = 0 - (*R)(0, 0) += steer_acc_r_cp4 * 1.0; - } -} + stop_watch_.tic(__func__); + + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t N_ref = ref_points.size(); + + const size_t D_v = D_x + (N_ref - 1) * D_u; + + const bool is_containing_path_terminal_point = points_utils::isNearLastPathPoint( + ref_points.back(), path_points, traj_param_.max_dist_for_extending_end_point, + traj_param_.delta_yaw_threshold_for_closest_point); + + // update Q + Eigen::SparseMatrix Qex_sparse_mat(D_x * N_ref, D_x * N_ref); + std::vector> Qex_triplet_vec; + for (size_t i = 0; i < N_ref; ++i) { + // this is for plan_from_ego + const bool near_kinematic_state_while_planning_from_ego = [&]() { + const size_t min_idx = static_cast(std::max(0, static_cast(i) - 20)); + const size_t max_idx = std::min(ref_points.size() - 1, i + 20); + for (size_t j = min_idx; j <= max_idx; ++j) { + if (ref_points.at(j).plan_from_ego && ref_points.at(j).fix_kinematic_state) { + return true; + } + } + return false; + }(); + + const auto adaptive_error_weight = [&]() -> std::array { + if (near_kinematic_state_while_planning_from_ego) { + constexpr double obstacle_avoid_error_weight_ratio = 1 / 100.0; + return { + mpt_param_.obstacle_avoid_lat_error_weight * obstacle_avoid_error_weight_ratio, + mpt_param_.obstacle_avoid_yaw_error_weight * obstacle_avoid_error_weight_ratio}; + } else if (ref_points.at(i).near_objects) { + return { + mpt_param_.obstacle_avoid_lat_error_weight, mpt_param_.obstacle_avoid_yaw_error_weight}; + } else if (i == N_ref - 1 && is_containing_path_terminal_point) { + return { + mpt_param_.terminal_path_lat_error_weight, mpt_param_.terminal_path_yaw_error_weight}; + } else if (i == N_ref - 1) { + return {mpt_param_.terminal_lat_error_weight, mpt_param_.terminal_yaw_error_weight}; + } + // NOTE: may be better to add decreasing weights in a narrow and sharp curve + // else if (std::abs(ref_points[i].k) > 0.3) { + // return {0.0, 0.0}; + // } -void MPTOptimizer::addSteerWeightF(Eigen::VectorXd * f) const -{ - constexpr double DT = 0.1; - constexpr double ctrl_period = 0.03; - constexpr double raw_steer_cmd_prev = 0; - constexpr double raw_steer_cmd_pprev = 0; + return {mpt_param_.lat_error_weight, mpt_param_.yaw_error_weight}; + }(); - if (f->rows() < 2) { - return; - } + const double adaptive_lat_error_weight = adaptive_error_weight.at(0); + const double adaptive_yaw_error_weight = adaptive_error_weight.at(1); - // steer rate for i = 0 - (*f)(0) += -2.0 * mpt_param_ptr_->steer_rate_weight / (std::pow(DT, 2)) * 0.5; + Qex_triplet_vec.push_back(Eigen::Triplet(i * D_x, i * D_x, adaptive_lat_error_weight)); + Qex_triplet_vec.push_back( + Eigen::Triplet(i * D_x + 1, i * D_x + 1, adaptive_yaw_error_weight)); + } + Qex_sparse_mat.setFromTriplets(Qex_triplet_vec.begin(), Qex_triplet_vec.end()); + + // update R + Eigen::SparseMatrix Rex_sparse_mat(D_v, D_v); + std::vector> Rex_triplet_vec; + for (size_t i = 0; i < N_ref - 1; ++i) { + const double adaptive_steer_weight = ref_points.at(i).near_objects + ? mpt_param_.obstacle_avoid_steer_input_weight + : mpt_param_.steer_input_weight; + Rex_triplet_vec.push_back( + Eigen::Triplet(D_x + D_u * i, D_x + D_u * i, adaptive_steer_weight)); + } + addSteerWeightR(Rex_triplet_vec, ref_points); - // const double steer_acc_r = mpt_param_.weight_steer_acc / std::pow(DT, 4); - const double steer_acc_r_cp1 = mpt_param_ptr_->steer_acc_weight / (std::pow(DT, 3) * ctrl_period); - const double steer_acc_r_cp2 = - mpt_param_ptr_->steer_acc_weight / (std::pow(DT, 2) * std::pow(ctrl_period, 2)); - const double steer_acc_r_cp4 = mpt_param_ptr_->steer_acc_weight / std::pow(ctrl_period, 4); + Rex_sparse_mat.setFromTriplets(Rex_triplet_vec.begin(), Rex_triplet_vec.end()); - // steer acc i = 0 - (*f)(0) += ((-2.0 * raw_steer_cmd_prev + raw_steer_cmd_pprev) * steer_acc_r_cp4) * 0.5; + ValueMatrix m; + m.Qex = Qex_sparse_mat; + m.Rex = Rex_sparse_mat; - // steer acc for i = 1 - (*f)(0) += (-2.0 * raw_steer_cmd_prev * (steer_acc_r_cp1 + steer_acc_r_cp2)) * 0.5; - (*f)(1) += (2.0 * raw_steer_cmd_prev * steer_acc_r_cp1) * 0.5; + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return m; } boost::optional MPTOptimizer::executeOptimization( - const bool enable_avoidance, const MPTMatrix & m, const std::vector & ref_points, - const std::vector & path_points, const CVMaps & maps, - DebugData * debug_data) + const std::unique_ptr & prev_trajs, const bool enable_avoidance, + const MPTMatrix & mpt_mat, const ValueMatrix & val_mat, + const std::vector & ref_points, std::shared_ptr debug_data_ptr) { - auto t_start1 = std::chrono::high_resolution_clock::now(); + if (ref_points.empty()) { + return Eigen::VectorXd{}; + } + + stop_watch_.tic(__func__); + + const size_t N_ref = ref_points.size(); - const auto x0 = ref_points.front().optimized_state; - ObjectiveMatrix obj_m = getObjectiveMatrix(x0, m); + // get matrix + ObjectiveMatrix obj_m = getObjectiveMatrix(mpt_mat, val_mat, ref_points, debug_data_ptr); ConstraintMatrix const_m = - getConstraintMatrix(enable_avoidance, x0, m, maps, ref_points, path_points, debug_data); + getConstraintMatrix(enable_avoidance, mpt_mat, ref_points, debug_data_ptr); + + // manual warm start + Eigen::VectorXd u0 = Eigen::VectorXd::Zero(obj_m.gradient.size()); + + if (mpt_param_.enable_manual_warm_start) { + const size_t D_x = vehicle_model_ptr_->getDimX(); + + if (prev_trajs && prev_trajs->mpt_ref_points.size() > 1) { + const size_t seg_idx = tier4_autoware_utils::findNearestSegmentIndex( + prev_trajs->mpt_ref_points, ref_points.front().p); + double offset = tier4_autoware_utils::calcLongitudinalOffsetToSegment( + prev_trajs->mpt_ref_points, seg_idx, ref_points.front().p); + + u0(0) = prev_trajs->mpt_ref_points.at(seg_idx).optimized_kinematic_state(0); + u0(1) = prev_trajs->mpt_ref_points.at(seg_idx).optimized_kinematic_state(1); + + for (size_t i = 0; i + 1 < N_ref; ++i) { + size_t prev_idx = seg_idx + i; + const size_t prev_N_ref = prev_trajs->mpt_ref_points.size(); + if (prev_idx + 2 > prev_N_ref) { + prev_idx = static_cast(prev_N_ref) - 2; + offset = 0.5; + } + + const double prev_val = prev_trajs->mpt_ref_points.at(prev_idx).optimized_input; + const double next_val = prev_trajs->mpt_ref_points.at(prev_idx + 1).optimized_input; + u0(D_x + i) = interpolation::lerp(prev_val, next_val, offset); + } + } + } + + const Eigen::MatrixXd & H = obj_m.hessian; + const Eigen::MatrixXd & A = const_m.linear; + std::vector f; + std::vector upper_bound; + std::vector lower_bound; + + if (mpt_param_.enable_manual_warm_start) { + f = eigenVectorToStdVector(obj_m.gradient + H * u0); + Eigen::VectorXd A_times_u0 = A * u0; + upper_bound = eigenVectorToStdVector(const_m.upper_bound - A_times_u0); + lower_bound = eigenVectorToStdVector(const_m.lower_bound - A_times_u0); + } else { + f = eigenVectorToStdVector(obj_m.gradient); + upper_bound = eigenVectorToStdVector(const_m.upper_bound); + lower_bound = eigenVectorToStdVector(const_m.lower_bound); + } + + // initialize or update solver with warm start + stop_watch_.tic("initOsqp"); + autoware::common::osqp::CSC_Matrix P_csc = autoware::common::osqp::calCSCMatrixTrapezoidal(H); + autoware::common::osqp::CSC_Matrix A_csc = autoware::common::osqp::calCSCMatrix(A); + if (mpt_param_.enable_warm_start && prev_mat_n == H.rows() && prev_mat_m == A.rows()) { + RCLCPP_INFO_EXPRESSION( + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "warm start"); + + osqp_solver_ptr_->updateCscP(P_csc); + osqp_solver_ptr_->updateQ(f); + osqp_solver_ptr_->updateCscA(A_csc); + osqp_solver_ptr_->updateL(lower_bound); + osqp_solver_ptr_->updateU(upper_bound); + } else { + RCLCPP_INFO_EXPRESSION( + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "no warm start"); + + osqp_solver_ptr_ = std::make_unique( + // obj_m.hessian, const_m.linear, obj_m.gradient, const_m.lower_bound, const_m.upper_bound, + P_csc, A_csc, f, lower_bound, upper_bound, osqp_epsilon_); + } + prev_mat_n = H.rows(); + prev_mat_m = A.rows(); - osqp_solver_ptr_ = std::make_unique( - obj_m.hessian, const_m.linear, obj_m.gradient, const_m.lower_bound, const_m.upper_bound, - 1.0e-3); - osqp_solver_ptr_->updateEpsRel(1.0e-3); + debug_data_ptr->msg_stream << " " + << "initOsqp" + << ":= " << stop_watch_.toc("initOsqp") << " [ms]\n"; + + // solve + stop_watch_.tic("solveOsqp"); const auto result = osqp_solver_ptr_->optimize(); + debug_data_ptr->msg_stream << " " + << "solveOsqp" + << ":= " << stop_watch_.toc("solveOsqp") << " [ms]\n"; - int solution_status = std::get<3>(result); + // check solution status + const int solution_status = std::get<3>(result); if (solution_status != 1) { - util::logOSQPSolutionStatus(solution_status); + utils::logOSQPSolutionStatus(solution_status); return boost::none; } + // print iteration + const int iteration_status = std::get<4>(result); + RCLCPP_INFO_EXPRESSION( + rclcpp::get_logger("mpt_optimizer"), is_showing_debug_info_, "iteration: %d", iteration_status); + + // get result std::vector result_vec = std::get<0>(result); + + const size_t DIM_U = vehicle_model_ptr_->getDimU(); + const size_t DIM_X = vehicle_model_ptr_->getDimX(); const Eigen::VectorXd optimized_control_variables = - Eigen::Map(&result_vec[0], ref_points.size()); + Eigen::Map(&result_vec[0], DIM_X + (N_ref - 1) * DIM_U); - auto t_end1 = std::chrono::high_resolution_clock::now(); - float elapsed_ms1 = std::chrono::duration(t_end1 - t_start1).count(); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, "mpt opt time: = %f [ms]", - elapsed_ms1); - return optimized_control_variables; -} + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; -double MPTOptimizer::calcLateralError( - const geometry_msgs::msg::Point & target_point, const ReferencePoint & ref_point) const -{ - const double err_x = target_point.x - ref_point.p.x; - const double err_y = target_point.y - ref_point.p.y; - const double ref_yaw = tf2::getYaw(ref_point.q); - const double lat_err = -std::sin(ref_yaw) * err_x + std::cos(ref_yaw) * err_y; - return lat_err; -} - -Eigen::VectorXd MPTOptimizer::getState( - const geometry_msgs::msg::Pose & target_pose, const ReferencePoint & nearest_ref_point) const -{ - const double lat_error = calcLateralError(target_pose.position, nearest_ref_point); - const double yaw_error = - util::normalizeRadian(tf2::getYaw(target_pose.orientation) - nearest_ref_point.yaw); - Eigen::VectorXd x0 = Eigen::VectorXd::Zero(3); - x0 << lat_error, yaw_error, std::atan(vehicle_param_ptr_->wheelbase * nearest_ref_point.k); - return x0; + const Eigen::VectorXd optimized_control_variables_with_offset = + mpt_param_.enable_manual_warm_start + ? optimized_control_variables + u0.segment(0, DIM_X + (N_ref - 1) * DIM_U) + : optimized_control_variables; + return optimized_control_variables_with_offset; } -std::vector MPTOptimizer::getMPTPoints( - std::vector & ref_points, const Eigen::VectorXd & Uex, - const MPTMatrix & mpt_matrix, - [[maybe_unused]] const std::vector & - optimized_points) const +MPTOptimizer::ObjectiveMatrix MPTOptimizer::getObjectiveMatrix( + const MPTMatrix & mpt_mat, const ValueMatrix & val_mat, + [[maybe_unused]] const std::vector & ref_points, + std::shared_ptr debug_data_ptr) const { - const int DIM_X = vehicle_model_ptr_->getDimX(); - const auto x0 = ref_points.front().optimized_state; - Eigen::VectorXd Xex = mpt_matrix.Aex * x0 + mpt_matrix.Bex * Uex + mpt_matrix.Wex; + stop_watch_.tic(__func__); - std::vector traj_points; - { - const double lat_error = x0(0); - autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; - traj_point.pose.position.x = ref_points[0].p.x - std::sin(ref_points[0].yaw) * lat_error; - traj_point.pose.position.y = ref_points[0].p.y + std::cos(ref_points[0].yaw) * lat_error; - traj_point.longitudinal_velocity_mps = ref_points.front().v; - traj_points.push_back(traj_point); - } + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t N_ref = ref_points.size(); - for (std::size_t i = 1; i < ref_points.size(); ++i) { - const double lat_error = Xex((i - 1) * DIM_X); - Eigen::Vector3d state = Xex.segment((i - 1) * DIM_X, DIM_X); - setOptimizedState(&ref_points[i], state); - autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; - traj_point.pose.position.x = ref_points[i].p.x - std::sin(ref_points[i].yaw) * lat_error; - traj_point.pose.position.y = ref_points[i].p.y + std::cos(ref_points[i].yaw) * lat_error; - traj_point.longitudinal_velocity_mps = ref_points[i].v; + const size_t D_xn = D_x * N_ref; + const size_t D_v = D_x + (N_ref - 1) * D_u; - traj_points.push_back(traj_point); - } - for (std::size_t i = 0; i < traj_points.size(); ++i) { - if (i > 0 && traj_points.size() > 1) { - traj_points[i].pose.orientation = util::getQuaternionFromPoints( - traj_points[i].pose.position, traj_points[i - 1].pose.position); - } else if (traj_points.size() > 1) { - traj_points[i].pose.orientation = util::getQuaternionFromPoints( - traj_points[i + 1].pose.position, traj_points[i].pose.position); - } else { - traj_points[i].pose.orientation = ref_points[i].q; - } - } - return traj_points; -} + // generate T matrix and vector to shift optimization center + // define Z as time-series vector of shifted deviation error + // Z = sparse_T_mat * (Bex * U + Wex) + T_vec + Eigen::SparseMatrix sparse_T_mat(D_xn, D_xn); + Eigen::VectorXd T_vec = Eigen::VectorXd::Zero(D_xn); + std::vector> triplet_T_vec; + const double offset = mpt_param_.optimization_center_offset; -void MPTOptimizer::setOptimizedState( - ReferencePoint * ref_point, const Eigen::Vector3d & optimized_state) const -{ - ref_point->optimized_state = optimized_state; -} + for (size_t i = 0; i < N_ref; ++i) { + const double alpha = ref_points.at(i).alpha; -std::vector MPTOptimizer::getReferenceBounds( - const bool enable_avoidance, const std::vector & ref_points, const CVMaps & maps, - DebugData * debug_data) const -{ - std::vector ref_bounds; - std::vector debug_bounds_candidate_for_base_points; - std::vector debug_bounds_candidate_for_top_points; - std::vector debug_bounds_candidate_for_mid_points; - int cnt = 0; - for (const auto & point : ref_points) { - ReferencePoint ref_base_point; - ref_base_point.p = point.p; - ref_base_point.yaw = point.yaw; - - ReferencePoint ref_top_point; - ref_top_point.p = point.top_pose.position; - ref_top_point.yaw = tf2::getYaw(point.top_pose.orientation); - - ReferencePoint ref_mid_point; - ref_mid_point.p = point.mid_pose.position; - ref_mid_point.yaw = tf2::getYaw(point.mid_pose.orientation); - - geometry_msgs::msg::Pose debug_for_base_point; - debug_for_base_point.position = ref_base_point.p; - debug_for_base_point.orientation = point.q; - debug_bounds_candidate_for_base_points.push_back(debug_for_base_point); - - geometry_msgs::msg::Pose debug_for_top_point; - debug_for_top_point.position = ref_top_point.p; - debug_for_top_point.orientation = point.top_pose.orientation; - debug_bounds_candidate_for_top_points.push_back(debug_for_top_point); - - geometry_msgs::msg::Pose debug_for_mid_point; - debug_for_mid_point.position = ref_mid_point.p; - debug_for_mid_point.orientation = point.top_pose.orientation; - debug_bounds_candidate_for_mid_points.push_back(debug_for_mid_point); - - if ( - !util::transformMapToOptionalImage(ref_base_point.p, maps.map_info) || - !util::transformMapToOptionalImage(ref_mid_point.p, maps.map_info) || - !util::transformMapToOptionalImage(ref_top_point.p, maps.map_info)) { - Bounds bounds; - bounds.c0 = {1, -1}; - bounds.c1 = {1, -1}; - bounds.c2 = {1, -1}; - ref_bounds.emplace_back(bounds); - continue; - } + triplet_T_vec.push_back(Eigen::Triplet(i * D_x, i * D_x, std::cos(alpha))); + triplet_T_vec.push_back(Eigen::Triplet(i * D_x, i * D_x + 1, offset * std::cos(alpha))); + triplet_T_vec.push_back(Eigen::Triplet(i * D_x + 1, i * D_x + 1, 1.0)); - // Calculate boundaries. - auto lat_bounds_0 = getBound(enable_avoidance, ref_base_point, maps); - auto lat_bounds_1 = getBound(enable_avoidance, ref_top_point, maps); - auto lat_bounds_2 = getBound(enable_avoidance, ref_mid_point, maps); - if (!lat_bounds_0 || !lat_bounds_1 || !lat_bounds_2) { - auto clock = rclcpp::Clock(RCL_ROS_TIME); - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("MPTOptimizer"), is_showing_debug_info_, "Path is blocked at %i ", cnt); - Bounds bounds; - bounds.c0 = {1, -1}; - bounds.c1 = {1, -1}; - bounds.c2 = {1, -1}; - ref_bounds.emplace_back(bounds); - cnt++; - continue; - } - Bounds bounds; - bounds.c0 = lat_bounds_0.get(); - bounds.c1 = lat_bounds_1.get(); - bounds.c2 = lat_bounds_2.get(); - ref_bounds.emplace_back(bounds); - cnt++; + T_vec(i * D_x) = -offset * std::sin(alpha); } - debug_data->bounds = ref_bounds; - debug_data->bounds_candidate_for_base_points = debug_bounds_candidate_for_base_points; - debug_data->bounds_candidate_for_top_points = debug_bounds_candidate_for_top_points; - debug_data->bounds_candidate_for_mid_points = debug_bounds_candidate_for_mid_points; - return ref_bounds; -} + sparse_T_mat.setFromTriplets(triplet_T_vec.begin(), triplet_T_vec.end()); -boost::optional> MPTOptimizer::getBound( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps) const -{ - const auto rough_road_bound = getRoughBound(enable_avoidance, ref_point, maps); + const Eigen::MatrixXd B = sparse_T_mat * mpt_mat.Bex; + const Eigen::MatrixXd QB = val_mat.Qex * B; + const Eigen::MatrixXd R = val_mat.Rex; - if (!rough_road_bound) { - return boost::none; - } + // min J(v) = min (v'Hv + v'f) + Eigen::MatrixXd H = Eigen::MatrixXd::Zero(D_v, D_v); + H.triangularView() = B.transpose() * QB + R; + H.triangularView() = H.transpose(); - std::vector broad_bound; - // initialize right bound with rough left bound - double rel_right_bound = rough_road_bound.get()[0]; - auto t_start = std::chrono::high_resolution_clock::now(); - float elapsed_ms = 0.0; - while (true) { - const auto bound_candidate = getBoundCandidate( - enable_avoidance, ref_point, maps, mpt_param_ptr_->clearance_from_road, - mpt_param_ptr_->clearance_from_object, rel_right_bound, rough_road_bound.get()); - - if (!bound_candidate) { - break; - } + // Eigen::VectorXd f = ((sparse_T_mat * mpt_mat.Wex + T_vec).transpose() * QB).transpose(); + Eigen::VectorXd f = (sparse_T_mat * mpt_mat.Wex + T_vec).transpose() * QB; - if (broad_bound.empty()) { - broad_bound = bound_candidate.get(); - } else { - const double bound_candidate_diff = - std::fabs(bound_candidate.get()[0] - bound_candidate.get()[1]); - const double broad_bound_diff = std::fabs(broad_bound[0] - broad_bound[1]); - if (bound_candidate_diff > broad_bound_diff) { - broad_bound = bound_candidate.get(); + const size_t N_avoid = mpt_param_.vehicle_circle_longitudinal_offsets.size(); + const size_t N_first_slack = [&]() -> size_t { + if (mpt_param_.soft_constraint) { + if (mpt_param_.l_inf_norm) { + return 1; } + return N_avoid; } - rel_right_bound = bound_candidate.get()[1]; - auto t_end = std::chrono::high_resolution_clock::now(); - elapsed_ms = std::chrono::duration(t_end - t_start).count(); - constexpr float max_ms = 10; - if (elapsed_ms > max_ms) { - // ROS_WARN_THROTTLE(1.0, "take too much time for calculating bound"); - return boost::none; + return 0; + }(); + const size_t N_second_slack = [&]() -> size_t { + if (mpt_param_.two_step_soft_constraint) { + return N_first_slack; } - } + return 0; + }(); - if (broad_bound.empty()) { - return boost::none; - } else { - return broad_bound; - } -} + // number of slack variables for one step + const size_t N_slack = N_first_slack + N_second_slack; -boost::optional> MPTOptimizer::getBoundCandidate( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps, - const double min_road_clearance, const double min_obj_clearance, const double rel_initial_lat, - const std::vector & rough_road_bound) const -{ - const bool search_expanding_side = true; - const auto left_bound = getValidLatError( - enable_avoidance, ref_point, rel_initial_lat, maps, min_road_clearance, min_obj_clearance, - rough_road_bound, search_expanding_side); - if (!left_bound) { - return boost::none; - } - constexpr double min_valid_lat_error = 0.1; - const double initial_right_bound = left_bound.get() - min_valid_lat_error; - const auto right_bound = getValidLatError( - enable_avoidance, ref_point, initial_right_bound, maps, min_road_clearance, min_obj_clearance, - rough_road_bound); - if (!right_bound) { - return boost::none; - } - if (std::fabs(left_bound.get() - right_bound.get()) < 1e-5) { - return boost::none; - } - std::vector bound{left_bound.get(), right_bound.get()}; - return bound; -} + // extend H for slack variables + Eigen::MatrixXd full_H = Eigen::MatrixXd::Zero(D_v + N_ref * N_slack, D_v + N_ref * N_slack); + full_H.block(0, 0, D_v, D_v) = H; -boost::optional> MPTOptimizer::getRoughBound( - const bool enable_avoidance, const ReferencePoint & ref_point, const CVMaps & maps) const -{ - double left_bound = 0; - double right_bound = 0; - const double left_angle = util::normalizeRadian(ref_point.yaw + M_PI_2); - const double right_angle = util::normalizeRadian(ref_point.yaw - M_PI_2); - - geometry_msgs::msg::Point new_position; - new_position.x = ref_point.p.x; - new_position.y = ref_point.p.y; - auto original_clearance = getClearance(maps.clearance_map, new_position, maps.map_info); - auto original_object_clearance = - getClearance(maps.only_objects_clearance_map, new_position, maps.map_info); - if (!original_clearance || !original_object_clearance) { - return boost::none; - } - if (!enable_avoidance) { - original_object_clearance.emplace(std::numeric_limits::max()); - } - constexpr double min_road_clearance = 0.1; - constexpr double min_obj_clearance = 0.1; - if ( - original_clearance.get() > min_road_clearance && - original_object_clearance.get() > min_obj_clearance) { - const double initial_dist = 0; - right_bound = -1 * getTraversedDistance( - enable_avoidance, ref_point, right_angle, initial_dist, maps, - min_road_clearance, min_obj_clearance); - left_bound = getTraversedDistance( - enable_avoidance, ref_point, left_angle, initial_dist, maps, min_road_clearance, - min_obj_clearance); - } else { - const double initial_dist = 0; - const bool search_expanding_side = true; - const double right_s = getTraversedDistance( - enable_avoidance, ref_point, right_angle, initial_dist, maps, min_road_clearance, - min_obj_clearance, search_expanding_side); - const double left_s = getTraversedDistance( - enable_avoidance, ref_point, left_angle, initial_dist, maps, min_road_clearance, - min_obj_clearance, search_expanding_side); - if (left_s < right_s) { - // Pick left side: - right_bound = left_s; - left_bound = getTraversedDistance( - enable_avoidance, ref_point, left_angle, left_s, maps, min_road_clearance, - min_obj_clearance); - } else { - // Pick right side: - left_bound = -right_s; - right_bound = -getTraversedDistance( - enable_avoidance, ref_point, right_angle, right_s, maps, min_road_clearance, - min_obj_clearance); - } - } - if (std::fabs(left_bound - right_bound) < 1e-6) { - return boost::none; - } - std::vector bound{left_bound, right_bound}; - return bound; -} + // extend f for slack variables + Eigen::VectorXd full_f(D_v + N_ref * N_slack); -boost::optional MPTOptimizer::getClearance( - const cv::Mat & clearance_map, const geometry_msgs::msg::Point & map_point, - const nav_msgs::msg::MapMetaData & map_info) const -{ - const auto image_point = util::transformMapToOptionalImage(map_point, map_info); - if (!image_point) { - return boost::none; + full_f.segment(0, D_v) = f; + if (N_first_slack > 0) { + full_f.segment(D_v, N_ref * N_first_slack) = + mpt_param_.soft_avoidance_weight * Eigen::VectorXd::Ones(N_ref * N_first_slack); + } + if (N_second_slack > 0) { + full_f.segment(D_v + N_ref * N_first_slack, N_ref * N_second_slack) = + mpt_param_.soft_second_avoidance_weight * Eigen::VectorXd::Ones(N_ref * N_second_slack); } - const float clearance = clearance_map.ptr(static_cast( - image_point.get().y))[static_cast(image_point.get().x)] * - map_info.resolution; - return clearance; -} -ObjectiveMatrix MPTOptimizer::getObjectiveMatrix( - const Eigen::VectorXd & x0, const MPTMatrix & m) const -{ - const int DIM_U_N = m.Uref_ex.rows(); - const Eigen::MatrixXd CB = m.Cex * m.Bex; - const Eigen::MatrixXd QCB = m.Qex * CB; - // Eigen::MatrixXd H = CB.transpose() * QCB + m.R1ex + m.R2ex; - // This calculation is heavy. looking for a good way. - Eigen::MatrixXd H = Eigen::MatrixXd::Zero(DIM_U_N, DIM_U_N); - H.triangularView() = CB.transpose() * QCB; - H.triangularView() += m.R1ex + m.R2ex; - H.triangularView() = H.transpose(); - Eigen::VectorXd f = - (m.Cex * (m.Aex * x0 + m.Wex)).transpose() * QCB - m.Uref_ex.transpose() * m.R1ex; - addSteerWeightF(&f); - - constexpr int num_lat_constraint = 3; - const int num_objective_variables = DIM_U_N * (1 + num_lat_constraint); - Eigen::MatrixXd extend_h = Eigen::MatrixXd::Zero(DIM_U_N, DIM_U_N); - Eigen::VectorXd extend_f = Eigen::VectorXd::Ones(DIM_U_N); - Eigen::MatrixXd concat_h = - Eigen::MatrixXd::Zero(num_objective_variables, num_objective_variables); - Eigen::VectorXd concat_f = Eigen::VectorXd::Zero(num_objective_variables); - concat_h.block(0, 0, DIM_U_N, DIM_U_N) = H; - concat_h.block(DIM_U_N, DIM_U_N, DIM_U_N, DIM_U_N) = extend_h; - concat_h.block(DIM_U_N * 2, DIM_U_N * 2, DIM_U_N, DIM_U_N) = extend_h; - concat_h.block(DIM_U_N * 3, DIM_U_N * 3, DIM_U_N, DIM_U_N) = extend_h; - concat_f << f, mpt_param_ptr_->base_point_weight * extend_f, - mpt_param_ptr_->top_point_weight * extend_f, mpt_param_ptr_->mid_point_weight * extend_f; ObjectiveMatrix obj_matrix; - obj_matrix.hessian = concat_h; - obj_matrix.gradient = {concat_f.data(), concat_f.data() + concat_f.rows()}; + obj_matrix.hessian = full_H; + obj_matrix.gradient = full_f; + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; return obj_matrix; } @@ -894,368 +890,737 @@ ObjectiveMatrix MPTOptimizer::getObjectiveMatrix( // Set constraint: lb <= Ax <= ub // decision variable // x := [u0, ..., uN-1 | z00, ..., z0N-1 | z10, ..., z1N-1 | z20, ..., z2N-1] -// \in \mathbb{R}^{N * (N_point + 1)} -ConstraintMatrix MPTOptimizer::getConstraintMatrix( - const bool enable_avoidance, const Eigen::VectorXd & x0, const MPTMatrix & m, const CVMaps & maps, +// \in \mathbb{R}^{N * (N_vehicle_circle + 1)} +MPTOptimizer::ConstraintMatrix MPTOptimizer::getConstraintMatrix( + [[maybe_unused]] const bool enable_avoidance, const MPTMatrix & mpt_mat, const std::vector & ref_points, - [[maybe_unused]] const std::vector & path_points, - DebugData * debug_data) const + [[maybe_unused]] std::shared_ptr debug_data_ptr) const { - std::vector dist_vec{ - mpt_param_ptr_->base_point_dist_from_base_link, mpt_param_ptr_->top_point_dist_from_base_link, - mpt_param_ptr_->mid_point_dist_from_base_link}; - - const size_t N_ref = m.Uref_ex.rows(); - const size_t N_state = vehicle_model_ptr_->getDimX(); - const size_t N_point = dist_vec.size(); - const size_t N_dec = N_ref * (N_point + 1); - - const auto bounds = getReferenceBounds(enable_avoidance, ref_points, maps, debug_data); - - Eigen::MatrixXd A = Eigen::MatrixXd::Zero(3 * N_ref * N_point + N_ref, N_dec); - Eigen::VectorXd lb = - Eigen::VectorXd::Constant(3 * N_ref * N_point + N_ref, -autoware::common::osqp::INF); - Eigen::VectorXd ub = - Eigen::VectorXd::Constant(3 * N_ref * N_point + N_ref, autoware::common::osqp::INF); - - // Define constraint matrices and vectors - // Gap from reference point around vehicle base_link - { - // C := [I | O | O] - Eigen::MatrixXd C = Eigen::MatrixXd::Zero(N_ref, N_ref * N_state); - for (size_t i = 0; i < N_ref; ++i) { - C(i, N_state * i) = 1; - } - // bias := Cast * (Aex * x0 + Wex) - Eigen::VectorXd bias = C * (m.Aex * x0 + m.Wex); - // A_blk := [C * Bex | I | O | O - // -C * Bex | I | O | O - // O | I | O | O] - Eigen::MatrixXd A_blk = Eigen::MatrixXd::Zero(3 * N_ref, N_dec); - A_blk.block(0, 0, N_ref, N_ref) = C * m.Bex; - A_blk.block(0, N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(N_ref, 0, N_ref, N_ref) = -C * m.Bex; - A_blk.block(N_ref, N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(2 * N_ref, N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - // lb_blk := [-bias + bounds.lb - // bias - bounds.ub - // 0] - Eigen::VectorXd lb_blk = Eigen::VectorXd::Zero(3 * N_ref); - for (size_t i = 0; i < N_ref; ++i) { - if (i == N_ref - 1) { - lb_blk(i) = -bias(i) + bounds[i].c0.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i].c0.ub; - } else { - lb_blk(i) = -bias(i) + bounds[i + 1].c0.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i + 1].c0.ub; - } - } - // Assign - A.block(0, 0, 3 * N_ref, N_dec) = A_blk; - lb.segment(0, 3 * N_ref) = lb_blk; - } + stop_watch_.tic(__func__); - // Gap from reference point around vehicle top - { - // C := diag([cos(alpha1) | l1*cos(alpha1) | 0]) - Eigen::MatrixXd C = Eigen::MatrixXd::Zero(N_ref, N_ref * N_state); - for (size_t i = 0; i < N_ref; ++i) { - Eigen::MatrixXd Cast = Eigen::MatrixXd::Zero(1, N_state); - if (i == N_ref - 1) { - Cast(0, 0) = std::cos(ref_points[i].delta_yaw_from_p1); - Cast(0, 1) = dist_vec[1] * std::cos(ref_points[i].delta_yaw_from_p1); - } else { - Cast(0, 0) = std::cos(ref_points[i + 1].delta_yaw_from_p1); - Cast(0, 1) = dist_vec[1] * std::cos(ref_points[i + 1].delta_yaw_from_p1); + // NOTE: currently, add additional length to soft bounds approximately + // for soft second and hard bounds + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t N_ref = ref_points.size(); + + const size_t N_u = (N_ref - 1) * D_u; + const size_t D_v = D_x + N_u; + + const size_t N_avoid = mpt_param_.vehicle_circle_longitudinal_offsets.size(); + + // number of slack variables for one step + const size_t N_first_slack = [&]() -> size_t { + if (mpt_param_.soft_constraint) { + if (mpt_param_.l_inf_norm) { + return 1; } - C.block(i, N_state * i, 1, N_state) = Cast; + return N_avoid; } - // bias := Cast * (Aex * x0 + Wex) - l1 * sin(alpha1) - Eigen::VectorXd bias = C * (m.Aex * x0 + m.Wex); - for (size_t i = 0; i < N_ref; ++i) { - if (i == N_ref - 1) { - bias(i) -= dist_vec[1] * std::sin(ref_points[i].delta_yaw_from_p1); - } else { - bias(i) -= dist_vec[1] * std::sin(ref_points[i + 1].delta_yaw_from_p1); - } + return 0; + }(); + const size_t N_second_slack = [&]() -> size_t { + if (mpt_param_.soft_constraint && mpt_param_.two_step_soft_constraint) { + return N_first_slack; } - // A_blk := [C * Bex | O | I | O - // -C * Bex | O | I | O - // O | O | I | O] - Eigen::MatrixXd A_blk = Eigen::MatrixXd::Zero(3 * N_ref, N_dec); - A_blk.block(0, 0, N_ref, N_ref) = C * m.Bex; - A_blk.block(0, 2 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(N_ref, 0, N_ref, N_ref) = -C * m.Bex; - A_blk.block(N_ref, 2 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(2 * N_ref, 2 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - // lb_blk := [-bias + bounds.lb - // bias - bounds.ub - // 0] - Eigen::VectorXd lb_blk = Eigen::VectorXd::Zero(3 * N_ref); - for (size_t i = 0; i < N_ref; ++i) { - if (i == N_ref - 1) { - lb_blk(i) = -bias(i) + bounds[i].c1.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i].c1.ub; - } else { - lb_blk(i) = -bias(i) + bounds[i + 1].c1.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i + 1].c1.ub; - } + return 0; + }(); + + // number of all slack variables is N_ref * N_slack + const size_t N_slack = N_first_slack + N_second_slack; + const size_t N_soft = mpt_param_.two_step_soft_constraint ? 2 : 1; + + const size_t A_cols = [&] { + if (mpt_param_.soft_constraint) { + return D_v + N_ref * N_slack; // initial_state + steer + soft + } + return D_v; // initial state + steer + }(); + + // calculate indices of fixed points + std::vector fixed_points_indices; + for (size_t i = 0; i < N_ref; ++i) { + if (ref_points.at(i).fix_kinematic_state) { + fixed_points_indices.push_back(i); } - // Assign - A.block(3 * N_ref, 0, 3 * N_ref, N_dec) = A_blk; - lb.segment(3 * N_ref, 3 * N_ref) = lb_blk; } - // Gap from reference point around vehicle middle - { - // C := [diag(cos(alpha2)) | diag(l2*cos(alpha2)) | O] - Eigen::MatrixXd C = Eigen::MatrixXd::Zero(N_ref, N_ref * N_state); + + // calculate rows of A + size_t A_rows = 0; + if (mpt_param_.soft_constraint) { + // 3 means slack variable constraints to be between lower and upper bounds, and positive. + A_rows += 3 * N_ref * N_avoid * N_soft; + } + if (mpt_param_.hard_constraint) { + A_rows += N_ref * N_avoid; + } + A_rows += fixed_points_indices.size() * D_x; + if (mpt_param_.steer_limit_constraint) { + A_rows += N_u; + } + + Eigen::MatrixXd A = Eigen::MatrixXd::Zero(A_rows, A_cols); + Eigen::VectorXd lb = Eigen::VectorXd::Constant(A_rows, -autoware::common::osqp::INF); + Eigen::VectorXd ub = Eigen::VectorXd::Constant(A_rows, autoware::common::osqp::INF); + size_t A_rows_end = 0; + + // CX = C(Bv + w) + C \in R^{N_ref, N_ref * D_x} + for (size_t l_idx = 0; l_idx < N_avoid; ++l_idx) { + // create C := [1 | l | O] + Eigen::SparseMatrix C_sparse_mat(N_ref, N_ref * D_x); + std::vector> C_triplet_vec; + Eigen::VectorXd C_vec = Eigen::VectorXd::Zero(N_ref); + + // calculate C mat and vec for (size_t i = 0; i < N_ref; ++i) { - Eigen::MatrixXd Cast = Eigen::MatrixXd::Zero(1, N_state); - if (i == N_ref - 1) { - Cast(0, 0) = std::cos(ref_points[i].delta_yaw_from_p2); - Cast(0, 1) = dist_vec[2] * std::cos(ref_points[i].delta_yaw_from_p2); - } else { - Cast(0, 0) = std::cos(ref_points[i + 1].delta_yaw_from_p2); - Cast(0, 1) = dist_vec[2] * std::cos(ref_points[i + 1].delta_yaw_from_p2); - } - C.block(i, N_state * i, 1, N_state) = Cast; + const double beta = ref_points.at(i).beta.at(l_idx).get(); + const double avoid_offset = mpt_param_.vehicle_circle_longitudinal_offsets.at(l_idx); + + C_triplet_vec.push_back(Eigen::Triplet(i, i * D_x, 1.0 * std::cos(beta))); + C_triplet_vec.push_back( + Eigen::Triplet(i, i * D_x + 1, avoid_offset * std::cos(beta))); + C_vec(i) = avoid_offset * std::sin(beta); } - // bias := Cast * (Aex * x0 + Wex) - l2 * sin(alpha2) - Eigen::VectorXd bias = C * (m.Aex * x0 + m.Wex); - for (size_t i = 0; i < N_ref; ++i) { - if (i == N_ref - 1) { - bias(i) -= dist_vec[2] * std::sin(ref_points[i].delta_yaw_from_p2); - } else { - bias(i) -= dist_vec[2] * std::sin(ref_points[i + 1].delta_yaw_from_p2); + C_sparse_mat.setFromTriplets(C_triplet_vec.begin(), C_triplet_vec.end()); + + // calculate CB, and CW + const Eigen::MatrixXd CB = C_sparse_mat * mpt_mat.Bex; + const Eigen::VectorXd CW = C_sparse_mat * mpt_mat.Wex + C_vec; + + // calculate bounds + const auto & [part_ub, part_lb] = extractBounds(ref_points, l_idx); + + // soft constraints + if (mpt_param_.soft_constraint) { + size_t A_offset_cols = D_v; + for (size_t s_idx = 0; s_idx < N_soft; ++s_idx) { + const size_t A_blk_rows = 3 * N_ref; + + // A := [C * Bex | O | ... | O | I | O | ... + // -C * Bex | O | ... | O | I | O | ... + // O | O | ... | O | I | O | ... ] + Eigen::MatrixXd A_blk = Eigen::MatrixXd::Zero(A_blk_rows, A_cols); + A_blk.block(0, 0, N_ref, D_v) = CB; + A_blk.block(N_ref, 0, N_ref, D_v) = -CB; + + size_t local_A_offset_cols = A_offset_cols; + if (!mpt_param_.l_inf_norm) { + local_A_offset_cols += N_ref * l_idx; + } + A_blk.block(0, local_A_offset_cols, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); + A_blk.block(N_ref, local_A_offset_cols, N_ref, N_ref) = + Eigen::MatrixXd::Identity(N_ref, N_ref); + A_blk.block(2 * N_ref, local_A_offset_cols, N_ref, N_ref) = + Eigen::MatrixXd::Identity(N_ref, N_ref); + + // lb := [lower_bound - CW + // CW - upper_bound + // O ] + Eigen::VectorXd lb_blk = Eigen::VectorXd::Zero(A_blk_rows); + lb_blk.segment(0, N_ref) = -CW + part_lb; + lb_blk.segment(N_ref, N_ref) = CW - part_ub; + + if (s_idx == 1) { + // add additional clearance + const double diff_clearance = + mpt_param_.soft_second_clearance_from_road - mpt_param_.soft_clearance_from_road; + lb_blk.segment(0, N_ref) -= Eigen::MatrixXd::Constant(N_ref, 1, diff_clearance); + lb_blk.segment(N_ref, N_ref) -= Eigen::MatrixXd::Constant(N_ref, 1, diff_clearance); + } + + A_offset_cols += N_ref * N_first_slack; + + A.block(A_rows_end, 0, A_blk_rows, A_cols) = A_blk; + lb.segment(A_rows_end, A_blk_rows) = lb_blk; + + A_rows_end += A_blk_rows; } } - // A_blk := [C * Bex | O | O | I - // -C * Bex | O | O | I - // O | O | O | I] - Eigen::MatrixXd A_blk = Eigen::MatrixXd::Zero(3 * N_ref, N_dec); - A_blk.block(0, 0, N_ref, N_ref) = C * m.Bex; - A_blk.block(0, 3 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(N_ref, 0, N_ref, N_ref) = -C * m.Bex; - A_blk.block(N_ref, 3 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - A_blk.block(2 * N_ref, 3 * N_ref, N_ref, N_ref) = Eigen::MatrixXd::Identity(N_ref, N_ref); - // lb_blk := [-bias + bounds.lb - // bias - bounds.ub - // 0] - Eigen::VectorXd lb_blk = Eigen::VectorXd::Zero(3 * N_ref); - for (size_t i = 0; i < N_ref; ++i) { - if (i == N_ref - 1) { - lb_blk(i) = -bias(i) + bounds[i].c2.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i].c2.ub; - } else { - lb_blk(i) = -bias(i) + bounds[i + 1].c2.lb; - lb_blk(N_ref + i) = bias(i) - bounds[i + 1].c2.ub; - } + + // hard constraints + if (mpt_param_.hard_constraint) { + const size_t A_blk_rows = N_ref; + + Eigen::MatrixXd A_blk = Eigen::MatrixXd::Zero(A_blk_rows, A_cols); + A_blk.block(0, 0, N_ref, N_ref) = CB; + + A.block(A_rows_end, 0, A_blk_rows, A_cols) = A_blk; + lb.segment(A_rows_end, A_blk_rows) = part_lb - CW; + ub.segment(A_rows_end, A_blk_rows) = part_ub - CW; + + A_rows_end += A_blk_rows; } - // Assign - A.block(2 * 3 * N_ref, 0, 3 * N_ref, N_dec) = A_blk; - lb.segment(2 * 3 * N_ref, 3 * N_ref) = lb_blk; } - // Fixed points constraint - { - // C := [I | O | O] - Eigen::MatrixXd C = Eigen::MatrixXd::Zero(N_ref, N_ref * N_state); - for (size_t i = 0; i < N_ref; ++i) { - C(i, N_state * i) = 1; - } - // bias := Cast * (Aex * x0 + Wex) - Eigen::VectorXd bias = C * (m.Aex * x0 + m.Wex); + // fixed points constraint + // CX = C(B v + w) where C extracts fixed points + if (fixed_points_indices.size() > 0) { + for (const size_t i : fixed_points_indices) { + A.block(A_rows_end, 0, D_x, D_v) = mpt_mat.Bex.block(i * D_x, 0, D_x, D_v); - // Assign - A.block(3 * N_point * N_ref, 0, N_ref, N_ref) = C * m.Bex; - for (size_t i = 0; i < N_ref; ++i) { - if (ref_points[i + 1].fix_state && i + 1 < N_ref) { - lb(3 * N_point * N_ref + i) = ref_points[i + 1].fix_state.get()(0) - bias(i); - ub(3 * N_point * N_ref + i) = ref_points[i + 1].fix_state.get()(0) - bias(i); - } else if (i == ref_points.size() - 1 && mpt_param_ptr_->is_hard_fixing_terminal_point) { - lb(3 * N_point * N_ref + i) = -bias(i); - ub(3 * N_point * N_ref + i) = -bias(i); - } + lb.segment(A_rows_end, D_x) = + ref_points[i].fix_kinematic_state.get() - mpt_mat.Wex.segment(i * D_x, D_x); + ub.segment(A_rows_end, D_x) = + ref_points[i].fix_kinematic_state.get() - mpt_mat.Wex.segment(i * D_x, D_x); + + A_rows_end += D_x; } } - ConstraintMatrix constraint_matrix; - - constraint_matrix.linear = A; + // steer max limit + if (mpt_param_.steer_limit_constraint) { + A.block(A_rows_end, D_x, N_u, N_u) = Eigen::MatrixXd::Identity(N_u, N_u); + lb.segment(A_rows_end, N_u) = Eigen::MatrixXd::Constant(N_u, 1, -mpt_param_.max_steer_rad); + ub.segment(A_rows_end, N_u) = Eigen::MatrixXd::Constant(N_u, 1, mpt_param_.max_steer_rad); - for (int i = 0; i < lb.size(); ++i) { - constraint_matrix.lower_bound.push_back(lb(i)); - constraint_matrix.upper_bound.push_back(ub(i)); + A_rows_end += N_u; } + ConstraintMatrix constraint_matrix; + constraint_matrix.linear = A; + constraint_matrix.lower_bound = lb; + constraint_matrix.upper_bound = ub; + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; return constraint_matrix; } -std::vector MPTOptimizer::getBaseReferencePoints( - const std::vector & interpolated_points, - const std::unique_ptr & prev_trajs, DebugData * debug_data) const +std::vector MPTOptimizer::getMPTPoints( + std::vector & fixed_ref_points, + std::vector & non_fixed_ref_points, const Eigen::VectorXd & Uex, + const MPTMatrix & mpt_mat, std::shared_ptr debug_data_ptr) { - std::vector reference_points; - for (const auto & e : interpolated_points) { - ReferencePoint ref_point; - ref_point.p = e; - reference_points.push_back(ref_point); - } - if (!prev_trajs) { - return reference_points; + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t N_ref = static_cast(Uex.rows() - D_x) + 1; + + stop_watch_.tic(__func__); + + std::vector lat_error_vec; + std::vector yaw_error_vec; + for (size_t i = 0; i < fixed_ref_points.size(); ++i) { + const auto & ref_point = fixed_ref_points.at(i); + + lat_error_vec.push_back(ref_point.fix_kinematic_state.get()(0)); + yaw_error_vec.push_back(ref_point.fix_kinematic_state.get()(1)); } - if (prev_trajs->model_predictive_trajectory.size() != prev_trajs->mpt_ref_points.size()) { - return reference_points; + + const size_t N_kinematic_state = vehicle_model_ptr_->getDimX(); + const Eigen::VectorXd Xex = mpt_mat.Bex * Uex + mpt_mat.Wex; + + for (size_t i = 0; i < non_fixed_ref_points.size(); ++i) { + lat_error_vec.push_back(Xex(i * N_kinematic_state)); + yaw_error_vec.push_back(Xex(i * N_kinematic_state + 1)); } - // re-calculating points' position for fixing - std::vector cropped_interpolated_points; - double accum_s_for_interpolated = 0; - for (std::size_t i = 0; i < interpolated_points.size(); i++) { - if (i > 0) { - accum_s_for_interpolated += - util::calculate2DDistance(interpolated_points[i], interpolated_points[i - 1]); + // calculate trajectory from optimization result + std::vector traj_points; + debug_data_ptr->vehicle_circles_pose.resize(lat_error_vec.size()); + for (size_t i = 0; i < lat_error_vec.size(); ++i) { + auto & ref_point = (i < fixed_ref_points.size()) + ? fixed_ref_points.at(i) + : non_fixed_ref_points.at(i - fixed_ref_points.size()); + const double lat_error = lat_error_vec.at(i); + const double yaw_error = yaw_error_vec.at(i); + + geometry_msgs::msg::Pose ref_pose; + ref_pose.position = ref_point.p; + ref_pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(ref_point.yaw); + debug_data_ptr->mpt_ref_poses.push_back(ref_pose); + debug_data_ptr->lateral_errors.push_back(lat_error); + + ref_point.optimized_kinematic_state << lat_error_vec.at(i), yaw_error_vec.at(i); + if (i >= fixed_ref_points.size()) { + const size_t j = i - fixed_ref_points.size(); + if (j == N_ref - 1) { + ref_point.optimized_input = 0.0; + } else { + ref_point.optimized_input = Uex(D_x + j * D_u); + } } - if ( - accum_s_for_interpolated > - traj_param_ptr_->num_sampling_points * traj_param_ptr_->delta_arc_length_for_mpt_points) { - break; + + autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; + traj_point.pose = calcVehiclePose(ref_point, lat_error, yaw_error, 0.0); + + traj_point.longitudinal_velocity_mps = ref_point.v; + traj_points.push_back(traj_point); + + { // for debug visualization + const double base_x = ref_point.p.x - std::sin(ref_point.yaw) * lat_error; + const double base_y = ref_point.p.y + std::cos(ref_point.yaw) * lat_error; + + // NOTE: coordinate of vehicle_circle_longitudinal_offsets is back wheel center + for (const double d : mpt_param_.vehicle_circle_longitudinal_offsets) { + geometry_msgs::msg::Pose vehicle_circle_pose; + + vehicle_circle_pose.position.x = base_x + d * std::cos(ref_point.yaw + yaw_error); + vehicle_circle_pose.position.y = base_y + d * std::sin(ref_point.yaw + yaw_error); + + vehicle_circle_pose.orientation = + tier4_autoware_utils::createQuaternionFromYaw(ref_point.yaw + ref_point.alpha); + + debug_data_ptr->vehicle_circles_pose.at(i).push_back(vehicle_circle_pose); + } } - cropped_interpolated_points.push_back(interpolated_points[i]); } - constexpr double fine_resolution = 0.005; - std::vector fine_interpolated_points; - fine_interpolated_points = - util::getInterpolatedPoints(cropped_interpolated_points, fine_resolution); - const double max_s = - traj_param_ptr_->backward_fixing_distance + traj_param_ptr_->forward_fixing_mpt_distance; - const int num_points = std::min( - static_cast(reference_points.size()), - static_cast(max_s / traj_param_ptr_->delta_arc_length_for_mpt_points)); - - for (int i = 0; i < num_points; i++) { - const int nearest_prev_idx = - util::getNearestPointIdx(prev_trajs->mpt_ref_points, reference_points[i].p); - if ( - util::calculate2DDistance( - prev_trajs->mpt_ref_points[nearest_prev_idx].p, reference_points[i].p) >= - std::fabs(traj_param_ptr_->delta_arc_length_for_mpt_points)) { + + // NOTE: If generated trajectory's orientation is not so smooth or kinematically infeasible, + // recalculate orientation here for (size_t i = 0; i < lat_error_vec.size(); ++i) { + // auto & ref_point = (i < fixed_ref_points.size()) + // ? fixed_ref_points.at(i) + // : non_fixed_ref_points.at(i - fixed_ref_points.size()); + // + // if (i > 0 && traj_points.size() > 1) { + // traj_points.at(i).pose.orientation = geometry_utils::getQuaternionFromPoints( + // traj_points.at(i).pose.position, traj_points.at(i - 1).pose.position); + // } else if (traj_points.size() > 1) { + // traj_points.at(i).pose.orientation = geometry_utils::getQuaternionFromPoints( + // traj_points.at(i + 1).pose.position, traj_points.at(i).pose.position); + // } else { + // traj_points.at(i).pose.orientation = + // tier4_autoware_utils::createQuaternionFromYaw(ref_point.yaw); + // } + // } + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + + return traj_points; +} + +void MPTOptimizer::calcOrientation(std::vector & ref_points) const +{ + const auto yaw_angles = slerpYawFromReferencePoints(ref_points); + for (size_t i = 0; i < ref_points.size(); ++i) { + if (ref_points.at(i).fix_kinematic_state) { continue; } - const int nearest_idx = - util::getNearestIdx(fine_interpolated_points, prev_trajs->mpt_ref_points[nearest_prev_idx].p); - if ( - util::calculate2DDistance( - fine_interpolated_points[nearest_idx], prev_trajs->mpt_ref_points[nearest_prev_idx].p) < - fine_resolution) { - reference_points[i] = prev_trajs->mpt_ref_points[nearest_prev_idx]; - reference_points[i].fix_state = prev_trajs->mpt_ref_points[nearest_prev_idx].optimized_state; + + ref_points.at(i).yaw = yaw_angles.at(i); + } +} + +void MPTOptimizer::calcVelocity( + std::vector & ref_points, + const std::vector & points) const +{ + for (size_t i = 0; i < ref_points.size(); i++) { + ref_points.at(i).v = points[tier4_autoware_utils::findNearestIndex(points, ref_points.at(i).p)] + .longitudinal_velocity_mps; + } +} + +void MPTOptimizer::calcCurvature(std::vector & ref_points) const +{ + const size_t num_points = static_cast(ref_points.size()); + + // calculate curvature by circle fitting from three points + size_t max_smoothing_num = static_cast(std::floor(0.5 * (num_points - 1))); + size_t L = + std::min(static_cast(mpt_param_.num_curvature_sampling_points), max_smoothing_num); + auto curvatures = points_utils::calcCurvature( + ref_points, static_cast(mpt_param_.num_curvature_sampling_points)); + for (size_t i = L; i < num_points - L; ++i) { + if (!ref_points.at(i).fix_kinematic_state) { + ref_points.at(i).k = curvatures.at(i); + } + } + // first and last curvature is copied from next value + for (size_t i = 0; i < std::min(L, num_points); ++i) { + if (!ref_points.at(i).fix_kinematic_state) { + ref_points.at(i).k = ref_points.at(std::min(L, num_points - 1)).k; + } + if (!ref_points.at(num_points - i - 1).fix_kinematic_state) { + ref_points.at(num_points - i - 1).k = + ref_points.at(std::max(static_cast(num_points) - static_cast(L) - 1, 0)).k; } } +} - std::vector trimmed_reference_points; - for (std::size_t i = 0; i < reference_points.size(); i++) { +void MPTOptimizer::calcArcLength(std::vector & ref_points) const +{ + for (size_t i = 0; i < ref_points.size(); i++) { if (i > 0) { - const double dx = reference_points[i].p.x - reference_points[i - 1].p.x; - const double dy = reference_points[i].p.y - reference_points[i - 1].p.y; - if (std::fabs(dx) < 1e-6 && std::fabs(dy) < 1e-6) { - continue; + geometry_msgs::msg::Point a, b; + a = ref_points.at(i).p; + b = ref_points.at(i - 1).p; + ref_points.at(i).s = ref_points.at(i - 1).s + tier4_autoware_utils::calcDistance2d(a, b); + } + } +} + +void MPTOptimizer::calcExtraPoints( + std::vector & ref_points, const std::unique_ptr & prev_trajs) const +{ + for (size_t i = 0; i < ref_points.size(); ++i) { + // alpha + const double front_wheel_s = + ref_points.at(i).s + + vehicle_param_.wheelbase; // TODO(murooka) use offset instead of wheelbase? + const int front_wheel_nearest_idx = points_utils::getNearestIdx(ref_points, front_wheel_s, i); + const auto front_wheel_pos = ref_points.at(front_wheel_nearest_idx).p; + + const bool are_too_close_points = + tier4_autoware_utils::calcDistance2d(front_wheel_pos, ref_points.at(i).p) < 1e-03; + const auto front_wheel_yaw = are_too_close_points ? ref_points.at(i).yaw + : tier4_autoware_utils::calcAzimuthAngle( + ref_points.at(i).p, front_wheel_pos); + ref_points.at(i).alpha = + tier4_autoware_utils::normalizeRadian(front_wheel_yaw - ref_points.at(i).yaw); + + // near objects + ref_points.at(i).near_objects = [&]() { + const int avoidance_check_steps = + mpt_param_.near_objects_length / mpt_param_.delta_arc_length_for_mpt_points; + + const int avoidance_check_begin_idx = + std::max(0, static_cast(i) - avoidance_check_steps); + const int avoidance_check_end_idx = + std::min(static_cast(ref_points.size()), static_cast(i) + avoidance_check_steps); + + for (int a_idx = avoidance_check_begin_idx; a_idx < avoidance_check_end_idx; ++a_idx) { + if (ref_points.at(a_idx).vehicle_bounds.at(0).hasCollisionWithObject()) { + return true; + } + } + return false; + }(); + + // The point are considered to be near the object if nearest previous ref point is near the + // object. + if (prev_trajs && !prev_trajs->mpt_ref_points.empty()) { + const auto & prev_ref_points = prev_trajs->mpt_ref_points; + + const size_t prev_idx = + tier4_autoware_utils::findNearestIndex(prev_ref_points, ref_points.at(i).p); + const double dist_to_nearest_prev_ref = + tier4_autoware_utils::calcDistance2d(prev_ref_points.at(prev_idx), ref_points.at(i)); + if (dist_to_nearest_prev_ref < 1.0 && prev_ref_points.at(prev_idx).near_objects) { + ref_points.at(i).near_objects = true; } } - trimmed_reference_points.push_back(reference_points[i]); } +} + +void MPTOptimizer::addSteerWeightR( + std::vector> & Rex_triplet_vec, + const std::vector & ref_points) const +{ + const size_t D_x = vehicle_model_ptr_->getDimX(); + const size_t D_u = vehicle_model_ptr_->getDimU(); + const size_t N_ref = ref_points.size(); + const size_t N_u = (N_ref - 1) * D_u; + const size_t D_v = D_x + N_u; + + // add steering rate : weight for (u(i) - u(i-1))^2 + for (size_t i = D_x; i < D_v - 1; ++i) { + Rex_triplet_vec.push_back(Eigen::Triplet(i, i, mpt_param_.steer_rate_weight)); + Rex_triplet_vec.push_back(Eigen::Triplet(i + 1, i, -mpt_param_.steer_rate_weight)); + Rex_triplet_vec.push_back(Eigen::Triplet(i, i + 1, -mpt_param_.steer_rate_weight)); + Rex_triplet_vec.push_back(Eigen::Triplet(i + 1, i + 1, mpt_param_.steer_rate_weight)); + } +} - for (const auto & e : trimmed_reference_points) { - if (e.fix_state) { - geometry_msgs::msg::Point rel_point; - rel_point.y = e.fix_state.get()(0); - geometry_msgs::msg::Pose origin; - origin.position = e.p; - origin.orientation = e.q; - geometry_msgs::msg::Pose debug_pose; - debug_pose.position = util::transformToAbsoluteCoordinate2D(rel_point, origin); - debug_data->fixed_mpt_points.push_back(debug_pose); - continue; +void MPTOptimizer::calcBounds( + std::vector & ref_points, const bool enable_avoidance, const CVMaps & maps, + std::shared_ptr debug_data_ptr) const +{ + stop_watch_.tic(__func__); + + // search bounds candidate for each ref points + SequentialBoundsCandidates sequential_bounds_candidates; + for (const auto & ref_point : ref_points) { + const auto bounds_candidates = getBoundsCandidates( + enable_avoidance, convertRefPointsToPose(ref_point), maps, debug_data_ptr); + sequential_bounds_candidates.push_back(bounds_candidates); + } + + // search continuous and widest bounds only for front point + for (size_t i = 0; i < sequential_bounds_candidates.size(); ++i) { + // NOTE: back() is the front avoiding circle + const auto & bounds_candidates = sequential_bounds_candidates.at(i); + const auto & ref_point = ref_points.at(i); + + // extract only continuous bounds; + if (i == 0) { // TODO(murooka) use previous bounds, not widest bounds + const auto & widest_bounds = findWidestBounds(bounds_candidates); + ref_points.at(i).bounds = widest_bounds; + } else { + const auto & prev_ref_point = ref_points.at(i - 1); + const auto & prev_continuous_bounds = prev_ref_point.bounds; + + // search continuous bounds + double max_length = std::numeric_limits::min(); + int max_idx = -1; + for (size_t c_idx = 0; c_idx < bounds_candidates.size(); ++c_idx) { + const auto & bounds_candidate = bounds_candidates.at(c_idx); + const double overlapped_length = calcOverlappedBounds( + convertRefPointsToPose(ref_point), bounds_candidate, + convertRefPointsToPose(prev_ref_point), prev_continuous_bounds); + if (overlapped_length > 0 && max_length < overlapped_length) { + max_length = overlapped_length; + max_idx = c_idx; + } + } + + // find widest bounds + if (max_idx == -1) { + // NOTE: set invalid bounds so that MPT won't be solved + // TODO(murooka) this invalid bounds even makes optimization solved. consider if this is ok + RCLCPP_WARN_EXPRESSION( + rclcpp::get_logger("getBounds: front"), is_showing_debug_info_, "invalid bounds"); + const auto invalid_bounds = + Bounds{-5.0, 5.0, CollisionType::OUT_OF_ROAD, CollisionType::OUT_OF_ROAD}; + ref_points.at(i).bounds = invalid_bounds; + } else { + ref_points.at(i).bounds = bounds_candidates.at(max_idx); + } } } - return trimmed_reference_points; + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return; } -double MPTOptimizer::getTraversedDistance( - const bool enable_avoidance, const ReferencePoint & ref_point, const double traverse_angle, - const double initial_value, const CVMaps & maps, const double min_road_clearance, - const double min_obj_clearance, const bool search_expanding_side) const +void MPTOptimizer::calcVehicleBounds( + std::vector & ref_points, [[maybe_unused]] const CVMaps & maps, + std::shared_ptr debug_data_ptr, [[maybe_unused]] const bool enable_avoidance) const { - constexpr double ds = 0.1; - constexpr double lane_width = 7.5; - int n = static_cast(lane_width / ds); - - double traversed_dist = initial_value; - for (int i = 0; i < n; ++i) { - traversed_dist += ds; - geometry_msgs::msg::Point new_position; - new_position.x = ref_point.p.x + traversed_dist * std::cos(traverse_angle); - new_position.y = ref_point.p.y + traversed_dist * std::sin(traverse_angle); - auto clearance = getClearance(maps.clearance_map, new_position, maps.map_info); - auto obj_clearance = getClearance(maps.only_objects_clearance_map, new_position, maps.map_info); - if (!clearance || !obj_clearance) { - clearance.emplace(0); - obj_clearance.emplace(0); + stop_watch_.tic(__func__); + + if (ref_points.size() == 1) { + for ([[maybe_unused]] const double d : mpt_param_.vehicle_circle_longitudinal_offsets) { + ref_points.at(0).vehicle_bounds.push_back(ref_points.at(0).bounds); + ref_points.at(0).beta.push_back(0.0); } - if (!enable_avoidance) { - obj_clearance.emplace(std::numeric_limits::max()); + return; + } + + SplineInterpolationPoints2d ref_points_spline_interpolation; + ref_points_spline_interpolation.calcSplineCoefficients(points_utils::convertToPoints(ref_points)); + + for (size_t p_idx = 0; p_idx < ref_points.size(); ++p_idx) { + const auto & ref_point = ref_points.at(p_idx); + ref_points.at(p_idx).vehicle_bounds.clear(); + ref_points.at(p_idx).beta.clear(); + + for (const double d : mpt_param_.vehicle_circle_longitudinal_offsets) { + geometry_msgs::msg::Pose avoid_traj_pose; + avoid_traj_pose.position = + ref_points_spline_interpolation.getSplineInterpolatedPoint(p_idx, d); + const double vehicle_bounds_pose_yaw = + ref_points_spline_interpolation.getSplineInterpolatedYaw(p_idx, d); + avoid_traj_pose.orientation = + tier4_autoware_utils::createQuaternionFromYaw(vehicle_bounds_pose_yaw); + + const double avoid_yaw = std::atan2( + avoid_traj_pose.position.y - ref_point.p.y, avoid_traj_pose.position.x - ref_point.p.x); + const double beta = ref_point.yaw - vehicle_bounds_pose_yaw; + ref_points.at(p_idx).beta.push_back(beta); + + const double offset_y = -tier4_autoware_utils::calcDistance2d(ref_point, avoid_traj_pose) * + std::sin(avoid_yaw - vehicle_bounds_pose_yaw); + + const auto vehicle_bounds_pose = + tier4_autoware_utils::calcOffsetPose(avoid_traj_pose, 0.0, offset_y, 0.0); + + // interpolate bounds + const double avoid_s = ref_points_spline_interpolation.getAccumulatedLength(p_idx) + d; + for (size_t cp_idx = p_idx; cp_idx < ref_points.size(); ++cp_idx) { + const double current_s = ref_points_spline_interpolation.getAccumulatedLength(cp_idx); + if (avoid_s <= current_s) { + double prev_avoid_idx; + if (cp_idx == 0) { + prev_avoid_idx = cp_idx; + } else { + prev_avoid_idx = cp_idx - 1; + } + + const double prev_s = + ref_points_spline_interpolation.getAccumulatedLength(prev_avoid_idx); + const double next_s = + ref_points_spline_interpolation.getAccumulatedLength(prev_avoid_idx + 1); + const double ratio = (avoid_s - prev_s) / (next_s - prev_s); + + const auto prev_bounds = ref_points.at(prev_avoid_idx).bounds; + const auto next_bounds = ref_points.at(prev_avoid_idx + 1).bounds; + + auto bounds = Bounds::lerp(prev_bounds, next_bounds, ratio); + bounds.translate(offset_y); + + ref_points.at(p_idx).vehicle_bounds.push_back(bounds); + break; + } + + if (cp_idx == ref_points.size() - 1) { + ref_points.at(p_idx).vehicle_bounds.push_back(ref_points.back().bounds); + } + } + + ref_points.at(p_idx).vehicle_bounds_poses.push_back(vehicle_bounds_pose); } - if (search_expanding_side) { - if (clearance.get() > min_road_clearance && obj_clearance.get() > min_obj_clearance) { - break; + } + + debug_data_ptr->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; +} + +BoundsCandidates MPTOptimizer::getBoundsCandidates( + const bool enable_avoidance, const geometry_msgs::msg::Pose & avoiding_point, const CVMaps & maps, + [[maybe_unused]] std::shared_ptr debug_data_ptr) const +{ + BoundsCandidates bounds_candidate; + + constexpr double max_search_lane_width = 5.0; + const std::vector ds_vec{0.45, 0.15, 0.05, 0.01}; + + // search right to left + const double bound_angle = + tier4_autoware_utils::normalizeRadian(tf2::getYaw(avoiding_point.orientation) + M_PI_2); + + double traversed_dist = -max_search_lane_width; + double current_right_bound = -max_search_lane_width; + + // calculate the initial position is empty or not + // 0.drivable, 1.out of map 2.out of road or object + CollisionType previous_collision_type = + getCollisionType(maps, enable_avoidance, avoiding_point, traversed_dist, bound_angle); + + const auto has_collision = [&](const CollisionType & collision_type) -> bool { + return collision_type == CollisionType::OUT_OF_ROAD || collision_type == CollisionType::OBJECT; + }; + CollisionType latest_right_bound_collision_type = CollisionType::OUT_OF_ROAD; + + while (traversed_dist < max_search_lane_width) { + for (size_t ds_idx = 0; ds_idx < ds_vec.size(); ++ds_idx) { + const double ds = ds_vec.at(ds_idx); + while (true) { + const CollisionType current_collision_type = + getCollisionType(maps, enable_avoidance, avoiding_point, traversed_dist, bound_angle); + + // return only full bounds whenever finding out of map + if (current_collision_type == CollisionType::OUT_OF_SIGHT) { + const auto full_bounds = Bounds{ + -max_search_lane_width, max_search_lane_width, CollisionType::OUT_OF_SIGHT, + CollisionType::OUT_OF_SIGHT}; + return BoundsCandidates({full_bounds}); + } + + if (has_collision(previous_collision_type)) { + if (!has_collision(current_collision_type)) { // if target_position becomes no collision + if (ds_idx == ds_vec.size() - 1) { + current_right_bound = traversed_dist - ds / 2.0; + latest_right_bound_collision_type = previous_collision_type; + previous_collision_type = current_collision_type; + } + break; + } + } else { + if (has_collision(current_collision_type)) { + if (ds_idx == ds_vec.size() - 1) { + const double left_bound = traversed_dist - ds / 2.0; + bounds_candidate.push_back(Bounds{ + current_right_bound, left_bound, latest_right_bound_collision_type, + current_collision_type}); + previous_collision_type = current_collision_type; + } + break; + } + } + + // if target_position is out of lane + if (traversed_dist >= max_search_lane_width) { + if (!has_collision(previous_collision_type) && ds_idx == ds_vec.size() - 1) { + const double left_bound = traversed_dist - ds / 2.0; + bounds_candidate.push_back(Bounds{ + current_right_bound, left_bound, latest_right_bound_collision_type, + CollisionType::OUT_OF_ROAD}); + } + break; + } + + // go forward with ds + traversed_dist += ds; + previous_collision_type = current_collision_type; } - } else { - if (clearance.get() < min_road_clearance || obj_clearance.get() < min_obj_clearance) { - break; + + if (ds_idx != ds_vec.size() - 1) { + // go back with ds since target_position became empty or road/object + // NOTE: if ds is the last of ds_vec, don't have to go back + traversed_dist -= ds; } } } - return traversed_dist; + + // if empty + // TODO(murooka) sometimes this condition realizes + if (bounds_candidate.empty()) { + RCLCPP_WARN_EXPRESSION( + rclcpp::get_logger("getBoundsCandidate"), is_showing_debug_info_, "empty bounds candidate"); + // NOTE: set invalid bounds so that MPT won't be solved + const auto invalid_bounds = + Bounds{-5.0, 5.0, CollisionType::OUT_OF_ROAD, CollisionType::OUT_OF_ROAD}; + bounds_candidate.push_back(invalid_bounds); + } + + return bounds_candidate; } -boost::optional MPTOptimizer::getValidLatError( - const bool enable_avoidance, const ReferencePoint & ref_point, const double initial_value, - const CVMaps & maps, const double min_road_clearance, const double min_obj_clearance, - const std::vector & rough_road_bound, const bool search_expanding_side) const +// 0.drivable, 1.out of drivable area 2.out of road or object +// 0.NO_COLLISION, 1.OUT_OF_SIGHT, 2.OUT_OF_ROAD, 3.OBJECT +CollisionType MPTOptimizer::getCollisionType( + const CVMaps & maps, const bool enable_avoidance, const geometry_msgs::msg::Pose & avoiding_point, + const double traversed_dist, const double bound_angle) const { - constexpr double ds = 0.01; - constexpr double lane_width = 7.5; - int n = static_cast(lane_width / ds); - - double rel_value = initial_value; - for (int i = 0; i < n; ++i) { - rel_value -= ds; - if (rel_value > rough_road_bound[0] || rel_value < rough_road_bound[1]) { - return boost::none; - } - geometry_msgs::msg::Point rel_point; - rel_point.y = rel_value; - geometry_msgs::msg::Pose origin; - origin.position = ref_point.p; - origin.orientation = util::getQuaternionFromYaw(ref_point.yaw); - const auto new_position = util::transformToAbsoluteCoordinate2D(rel_point, origin); - auto clearance = getClearance(maps.clearance_map, new_position, maps.map_info); - auto obj_clearance = getClearance(maps.only_objects_clearance_map, new_position, maps.map_info); - if (!clearance || !obj_clearance) { - return boost::none; + // calculate clearance + const double min_soft_road_clearance = mpt_param_.vehicle_circle_radius + + mpt_param_.soft_clearance_from_road + + mpt_param_.extra_desired_clearance_from_road; + const double min_obj_clearance = mpt_param_.vehicle_circle_radius + + mpt_param_.clearance_from_object + + mpt_param_.soft_clearance_from_road; + + // calculate target position + const geometry_msgs::msg::Point target_pos = tier4_autoware_utils::createPoint( + avoiding_point.position.x + traversed_dist * std::cos(bound_angle), + avoiding_point.position.y + traversed_dist * std::sin(bound_angle), 0.0); + + const auto opt_road_clearance = getClearance(maps.clearance_map, target_pos, maps.map_info); + const auto opt_obj_clearance = + getClearance(maps.only_objects_clearance_map, target_pos, maps.map_info); + + // object has more priority than road, so its condition exists first + if (enable_avoidance && opt_obj_clearance) { + const bool is_obj = opt_obj_clearance.get() < min_obj_clearance; + if (is_obj) { + return CollisionType::OBJECT; } - if (!enable_avoidance) { - obj_clearance.emplace(std::numeric_limits::max()); - } - if (search_expanding_side) { - if (clearance.get() > min_road_clearance && obj_clearance.get() > min_obj_clearance) { - break; - } + } + + if (opt_road_clearance) { + const bool out_of_road = opt_road_clearance.get() < min_soft_road_clearance; + if (out_of_road) { + return CollisionType::OUT_OF_ROAD; } else { - if (clearance.get() < min_road_clearance || obj_clearance.get() < min_obj_clearance) { - break; - } + return CollisionType::NO_COLLISION; } } - return rel_value; + + return CollisionType::OUT_OF_SIGHT; +} + +boost::optional MPTOptimizer::getClearance( + const cv::Mat & clearance_map, const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & map_info) const +{ + const auto image_point = geometry_utils::transformMapToOptionalImage(map_point, map_info); + if (!image_point) { + return boost::none; + } + const float clearance = clearance_map.ptr(static_cast( + image_point.get().y))[static_cast(image_point.get().x)] * + map_info.resolution; + return clearance; } diff --git a/planning/obstacle_avoidance_planner/src/node.cpp b/planning/obstacle_avoidance_planner/src/node.cpp index 68064d2be7896..78abaf7ca50c5 100644 --- a/planning/obstacle_avoidance_planner/src/node.cpp +++ b/planning/obstacle_avoidance_planner/src/node.cpp @@ -14,32 +14,20 @@ #include "obstacle_avoidance_planner/node.hpp" -#include "obstacle_avoidance_planner/debug.hpp" -#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/mpt_optimizer.hpp" -#include "obstacle_avoidance_planner/process_cv.hpp" -#include "obstacle_avoidance_planner/util.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include +#include "interpolation/spline_interpolation_points_2d.hpp" +#include "obstacle_avoidance_planner/cv_utils.hpp" +#include "obstacle_avoidance_planner/debug_visualization.hpp" +#include "obstacle_avoidance_planner/utils.hpp" +#include "rclcpp/time.hpp" +#include "tf2/utils.h" +#include "tier4_autoware_utils/ros/update_param.hpp" +#include "tier4_autoware_utils/trajectory/tmp_conversion.hpp" +#include "vehicle_info_util/vehicle_info_util.hpp" + +#include "geometry_msgs/msg/transform_stamped.hpp" +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "std_msgs/msg/bool.hpp" +#include "visualization_msgs/msg/marker_array.hpp" #include #include @@ -48,25 +36,175 @@ #include #include +namespace +{ +template +size_t searchExtendedZeroVelocityIndex( + const std::vector & fine_points, const std::vector & vel_points) +{ + const auto opt_zero_vel_idx = tier4_autoware_utils::searchZeroVelocityIndex(vel_points); + const size_t zero_vel_idx = opt_zero_vel_idx ? opt_zero_vel_idx.get() : vel_points.size() - 1; + return tier4_autoware_utils::findNearestIndex( + fine_points, vel_points.at(zero_vel_idx).pose.position); +} + +bool isPathShapeChanged( + const geometry_msgs::msg::Pose & ego_pose, + const std::vector & path_points, + const std::unique_ptr> & + prev_path_points, + const double max_mpt_length, const double max_path_shape_change_dist, + const double delta_yaw_threshold) +{ + if (!prev_path_points) { + return false; + } + + // truncate prev points from ego pose to fixed end points + const auto opt_prev_begin_idx = tier4_autoware_utils::findNearestIndex( + *prev_path_points, ego_pose, std::numeric_limits::max(), delta_yaw_threshold); + const size_t prev_begin_idx = opt_prev_begin_idx ? *opt_prev_begin_idx : 0; + const auto truncated_prev_points = + points_utils::clipForwardPoints(*prev_path_points, prev_begin_idx, max_mpt_length); + + // truncate points from ego pose to fixed end points + const auto opt_begin_idx = tier4_autoware_utils::findNearestIndex( + path_points, ego_pose, std::numeric_limits::max(), delta_yaw_threshold); + const size_t begin_idx = opt_begin_idx ? *opt_begin_idx : 0; + const auto truncated_points = + points_utils::clipForwardPoints(path_points, begin_idx, max_mpt_length); + + // guard for lateral offset + if (truncated_prev_points.size() < 2 || truncated_points.size() < 2) { + return false; + } + + // calculate lateral deviations between truncated path_points and prev_path_points + for (const auto & prev_point : truncated_prev_points) { + const double dist = + tier4_autoware_utils::calcLateralOffset(truncated_points, prev_point.pose.position); + if (dist > max_path_shape_change_dist) { + return true; + } + } + + return false; +} + +bool isPathGoalChanged( + const double current_vel, + const std::vector & path_points, + const std::unique_ptr> & + prev_path_points) +{ + if (!prev_path_points) { + return false; + } + + constexpr double min_vel = 1e-3; + if (std::abs(current_vel) > min_vel) { + return false; + } + + // NOTE: Path may be cropped and does not contain the goal. + // Therefore we set a large value to distance threshold. + constexpr double max_goal_moving_dist = 1.0; + const double goal_moving_dist = + tier4_autoware_utils::calcDistance2d(path_points.back(), prev_path_points->back()); + if (goal_moving_dist < max_goal_moving_dist) { + return false; + } + + return true; +} + +bool hasValidNearestPointFromEgo( + const geometry_msgs::msg::Pose & ego_pose, const Trajectories & trajs, + const TrajectoryParam & traj_param) +{ + const auto traj = trajs.model_predictive_trajectory; + const auto interpolated_points = + interpolation_utils::getInterpolatedPoints(traj, traj_param.delta_arc_length_for_trajectory); + + const auto interpolated_poses_with_yaw = + points_utils::convertToPosesWithYawEstimation(interpolated_points); + const auto opt_nearest_idx = tier4_autoware_utils::findNearestIndex( + interpolated_poses_with_yaw, ego_pose, traj_param.delta_dist_threshold_for_closest_point, + traj_param.delta_yaw_threshold_for_closest_point); + + if (!opt_nearest_idx) { + return false; + } + return true; +} + +std::tuple> calcVehicleCirclesInfo( + const VehicleParam & vehicle_param, const size_t circle_num_for_constraints, + const size_t circle_num_for_radius, const double radius_ratio) +{ + const double radius = std::hypot( + vehicle_param.length / static_cast(circle_num_for_radius) / 2.0, + vehicle_param.width / 2.0) * + radius_ratio; + + std::vector longitudinal_offsets; + const double unit_lon_length = vehicle_param.length / static_cast(circle_num_for_radius); + for (size_t i = 0; i < circle_num_for_constraints; ++i) { + longitudinal_offsets.push_back( + unit_lon_length / 2.0 + + (unit_lon_length * (circle_num_for_radius - 1)) / + static_cast(circle_num_for_constraints - 1) * i - + vehicle_param.rear_overhang); + } + + return {radius, longitudinal_offsets}; +} + +[[maybe_unused]] void fillYawInTrajectoryPoint( + std::vector & traj_points) +{ + std::vector points; + for (const auto & traj_point : traj_points) { + points.push_back(traj_point.pose.position); + } + const auto yaw_vec = interpolation::slerpYawFromPoints(points); + + for (size_t i = 0; i < traj_points.size(); ++i) { + traj_points.at(i).pose.orientation = + tier4_autoware_utils::createQuaternionFromYaw(yaw_vec.at(i)); + } +} + +} // namespace + ObstacleAvoidancePlanner::ObstacleAvoidancePlanner(const rclcpp::NodeOptions & node_options) -: Node("obstacle_avoidance_planner", node_options), min_num_points_for_getting_yaw_(2) +: Node("obstacle_avoidance_planner", node_options), + logger_ros_clock_(RCL_ROS_TIME), + eb_solved_count_(0) { rclcpp::Clock::SharedPtr clock = std::make_shared(RCL_ROS_TIME); - tf_buffer_ptr_ = std::make_unique(clock); - tf_listener_ptr_ = std::make_unique(*tf_buffer_ptr_); + // qos rclcpp::QoS durable_qos{1}; durable_qos.transient_local(); - trajectory_pub_ = - create_publisher("~/output/path", 1); - avoiding_traj_pub_ = create_publisher( - "/planning/scenario_planning/lane_driving/obstacle_avoidance_candidate_trajectory", - durable_qos); - debug_smoothed_points_pub_ = create_publisher( - "~/debug/smoothed_points", durable_qos); - is_avoidance_possible_pub_ = create_publisher( - "/planning/scenario_planning/lane_driving/obstacle_avoidance_ready", durable_qos); + // publisher to other nodes + traj_pub_ = create_publisher("~/output/path", 1); + + // debug publisher + debug_eb_traj_pub_ = create_publisher( + "~/debug/eb_trajectory", durable_qos); + debug_extended_fixed_traj_pub_ = create_publisher( + "~/debug/extended_fixed_traj", 1); + debug_extended_non_fixed_traj_pub_ = + create_publisher( + "~/debug/extended_non_fixed_traj", 1); + debug_mpt_fixed_traj_pub_ = + create_publisher("~/debug/mpt_fixed_traj", 1); + debug_mpt_ref_traj_pub_ = + create_publisher("~/debug/mpt_ref_traj", 1); + debug_mpt_traj_pub_ = + create_publisher("~/debug/mpt_traj", 1); debug_markers_pub_ = create_publisher("~/debug/marker", durable_qos); debug_clearance_map_pub_ = @@ -75,7 +213,10 @@ ObstacleAvoidancePlanner::ObstacleAvoidancePlanner(const rclcpp::NodeOptions & n create_publisher("~/debug/object_clearance_map", durable_qos); debug_area_with_objects_pub_ = create_publisher("~/debug/area_with_objects", durable_qos); + debug_msg_pub_ = + create_publisher("~/debug/calculation_time", 1); + // subscriber path_sub_ = create_subscription( "~/input/path", rclcpp::QoS{1}, std::bind(&ObstacleAvoidancePlanner::pathCallback, this, std::placeholders::_1)); @@ -89,213 +230,576 @@ ObstacleAvoidancePlanner::ObstacleAvoidancePlanner(const rclcpp::NodeOptions & n "/planning/scenario_planning/lane_driving/obstacle_avoidance_approval", rclcpp::QoS{10}, std::bind(&ObstacleAvoidancePlanner::enableAvoidanceCallback, this, std::placeholders::_1)); - is_publishing_area_with_objects_ = declare_parameter("is_publishing_area_with_objects", false); - is_publishing_clearance_map_ = declare_parameter("is_publishing_clearance_map", false); - is_showing_debug_info_ = declare_parameter("is_showing_debug_info", false); - is_using_vehicle_config_ = declare_parameter("is_using_vehicle_config", false); - is_stopping_if_outside_drivable_area_ = - declare_parameter("is_stopping_if_outside_drivable_area", true); - enable_avoidance_ = declare_parameter("enable_avoidance", true); - - qp_param_ = std::make_unique(); - traj_param_ = std::make_unique(); - constrain_param_ = std::make_unique(); - vehicle_param_ = std::make_unique(); - mpt_param_ = std::make_unique(); - qp_param_->max_iteration = declare_parameter("qp_max_iteration", 10000); - qp_param_->eps_abs = declare_parameter("qp_eps_abs", 1.0e-8); - qp_param_->eps_rel = declare_parameter("qp_eps_rel", 1.0e-11); - qp_param_->eps_abs_for_extending = declare_parameter("qp_eps_abs_for_extending", 1.0e-6); - qp_param_->eps_rel_for_extending = declare_parameter("qp_eps_rel_for_extending", 1.0e-8); - qp_param_->eps_abs_for_visualizing = declare_parameter("qp_eps_abs_for_visualizing", 1.0e-6); - qp_param_->eps_rel_for_visualizing = declare_parameter("qp_eps_rel_for_visualizing", 1.0e-8); - - traj_param_->num_sampling_points = declare_parameter("num_sampling_points", 100); - traj_param_->num_joint_buffer_points = declare_parameter("num_joint_buffer_points", 2); - traj_param_->num_joint_buffer_points_for_extending = - declare_parameter("num_joint_buffer_points_for_extending", 4); - traj_param_->num_offset_for_begin_idx = declare_parameter("num_offset_for_begin_idx", 2); - traj_param_->num_fix_points_for_extending = declare_parameter("num_fix_points_for_extending", 2); - traj_param_->forward_fixing_mpt_distance = declare_parameter("forward_fixing_mpt_distance", 10); - traj_param_->delta_arc_length_for_optimization = - declare_parameter("delta_arc_length_for_optimization", 1.0); - traj_param_->delta_arc_length_for_mpt_points = - declare_parameter("delta_arc_length_for_mpt_points", 1.0); - traj_param_->delta_arc_length_for_trajectory = - declare_parameter("delta_arc_length_for_trajectory", 0.1); - traj_param_->delta_dist_threshold_for_closest_point = - declare_parameter("delta_dist_threshold_for_closest_point", 3.0); - traj_param_->delta_yaw_threshold_for_closest_point = - declare_parameter("delta_yaw_threshold_for_closest_point", 1.0); - traj_param_->delta_yaw_threshold_for_straight = - declare_parameter("delta_yaw_threshold_for_straight", 0.02); - traj_param_->trajectory_length = declare_parameter("trajectory_length", 200); - traj_param_->forward_fixing_distance = declare_parameter("forward_fixing_distance", 10.0); - traj_param_->backward_fixing_distance = declare_parameter("backward_fixing_distance", 5.0); - traj_param_->max_avoiding_ego_velocity_ms = - declare_parameter("max_avoiding_ego_velocity_ms", 6.0); - traj_param_->max_avoiding_objects_velocity_ms = - declare_parameter("max_avoiding_objects_velocity_ms", 0.1); - traj_param_->center_line_width = declare_parameter("center_line_width", 1.7); - traj_param_->acceleration_for_non_deceleration_range = - declare_parameter("acceleration_for_non_deceleration_range", 1.0); - traj_param_->max_dist_for_extending_end_point = - declare_parameter("max_dist_for_extending_end_point", 5.0); - traj_param_->is_avoiding_unknown = declare_parameter("avoiding_object_type.unknown", true); - traj_param_->is_avoiding_car = declare_parameter("avoiding_object_type.car", true); - traj_param_->is_avoiding_truck = declare_parameter("avoiding_object_type.truck", true); - traj_param_->is_avoiding_bus = declare_parameter("avoiding_object_type.bus", true); - traj_param_->is_avoiding_bicycle = declare_parameter("avoiding_object_type.bicycle", true); - traj_param_->is_avoiding_motorbike = declare_parameter("avoiding_object_type.motorbike", true); - traj_param_->is_avoiding_pedestrian = declare_parameter("avoiding_object_type.pedestrian", true); - traj_param_->is_avoiding_animal = declare_parameter("avoiding_object_type.animal", true); - - constrain_param_->is_getting_constraints_close2path_points = - declare_parameter("is_getting_constraints_close2path_points", false); - constrain_param_->clearance_for_straight_line = - declare_parameter("clearance_for_straight_line", 0.05); - constrain_param_->clearance_for_joint = declare_parameter("clearance_for_joint", 0.1); - constrain_param_->range_for_extend_joint = declare_parameter("range_for_extend_joint", 1.6); - constrain_param_->clearance_for_only_smoothing = - declare_parameter("clearance_for_only_smoothing", 0.1); - constrain_param_->clearance_from_object_for_straight = - declare_parameter("clearance_from_object_for_straight", 10.0); - constrain_param_->clearance_from_road = declare_parameter("clearance_from_road", 0.1); - constrain_param_->clearance_from_object = declare_parameter("clearance_from_object", 0.6); - constrain_param_->extra_desired_clearance_from_road = - declare_parameter("extra_desired_clearance_from_road", 0.2); - constrain_param_->min_object_clearance_for_joint = - declare_parameter("min_object_clearance_for_joint", 3.2); - constrain_param_->max_x_constrain_search_range = - declare_parameter("max_x_constrain_search_range", 0.4); - constrain_param_->coef_x_constrain_search_resolution = - declare_parameter("coef_x_constrain_search_resolution", 1.0); - constrain_param_->coef_y_constrain_search_resolution = - declare_parameter("coef_y_constrain_search_resolution", 0.5); - constrain_param_->keep_space_shape_x = declare_parameter("keep_space_shape_x", 3.0); - constrain_param_->keep_space_shape_y = declare_parameter("keep_space_shape_y", 2.0); - constrain_param_->max_lon_space_for_driveable_constraint = - declare_parameter("max_lon_space_for_driveable_constraint", 0.5); - constrain_param_->clearance_for_fixing = 0.0; - - min_delta_dist_for_replan_ = declare_parameter("min_delta_dist_for_replan", 5.0); - min_delta_time_sec_for_replan_ = declare_parameter("min_delta_time_sec_for_replan", 1.0); - distance_for_path_shape_change_detection_ = - declare_parameter("distance_for_path_shape_change_detection", 2.0); - - // vehicle param - const auto vehicle_info = vehicle_info_util::VehicleInfoUtil(*this).getVehicleInfo(); - vehicle_param_->width = vehicle_info.vehicle_width_m; - vehicle_param_->length = vehicle_info.vehicle_length_m; - vehicle_param_->wheelbase = vehicle_info.wheel_base_m; - vehicle_param_->rear_overhang = vehicle_info.rear_overhang_m; - vehicle_param_->front_overhang = vehicle_info.front_overhang_m; - - if (is_using_vehicle_config_) { - double vehicle_width = vehicle_info.vehicle_width_m; - traj_param_->center_line_width = vehicle_width; - constrain_param_->keep_space_shape_y = vehicle_width; - } - constrain_param_->min_object_clearance_for_deceleration = - constrain_param_->clearance_from_object + constrain_param_->keep_space_shape_y * 0.5; - - double max_steer_deg = 0; - max_steer_deg = declare_parameter("max_steer_deg", 30.0); - vehicle_param_->max_steer_rad = max_steer_deg * M_PI / 180.0; - vehicle_param_->steer_tau = declare_parameter("steer_tau", 0.1); - - // mpt param - mpt_param_->is_hard_fixing_terminal_point = - declare_parameter("is_hard_fixing_terminal_point", true); - mpt_param_->num_curvature_sampling_points = declare_parameter("num_curvature_sampling_points", 5); - mpt_param_->base_point_weight = declare_parameter("base_point_weight", 2000.0); - mpt_param_->top_point_weight = declare_parameter("top_point_weight", 1000.0); - mpt_param_->mid_point_weight = declare_parameter("mid_point_weight", 1000.0); - mpt_param_->lat_error_weight = declare_parameter("lat_error_weight", 10.0); - mpt_param_->yaw_error_weight = declare_parameter("yaw_error_weight", 0.0); - mpt_param_->steer_input_weight = declare_parameter("steer_input_weight", 0.1); - mpt_param_->steer_rate_weight = declare_parameter("steer_rate_weight", 100.0); - mpt_param_->steer_acc_weight = declare_parameter("steer_acc_weight", 0.000001); - mpt_param_->terminal_lat_error_weight = declare_parameter("terminal_lat_error_weight", 0.0); - mpt_param_->terminal_yaw_error_weight = declare_parameter("terminal_yaw_error_weight", 100.0); - mpt_param_->terminal_path_lat_error_weight = - declare_parameter("terminal_path_lat_error_weight", 1000.0); - mpt_param_->terminal_path_yaw_error_weight = - declare_parameter("terminal_path_yaw_error_weight", 1000.0); - mpt_param_->zero_ff_steer_angle = declare_parameter("zero_ff_steer_angle", 0.5); - - mpt_param_->clearance_from_road = vehicle_param_->width * 0.5 + - constrain_param_->clearance_from_road + - constrain_param_->extra_desired_clearance_from_road; - mpt_param_->clearance_from_object = - vehicle_param_->width * 0.5 + constrain_param_->clearance_from_object; - mpt_param_->base_point_dist_from_base_link = 0; - mpt_param_->top_point_dist_from_base_link = - (vehicle_param_->length - vehicle_param_->rear_overhang); - mpt_param_->mid_point_dist_from_base_link = - (mpt_param_->base_point_dist_from_base_link + mpt_param_->top_point_dist_from_base_link) * 0.5; - - in_objects_ptr_ = std::make_unique(); + { // vehicle param + vehicle_param_ = VehicleParam{}; + const auto vehicle_info = vehicle_info_util::VehicleInfoUtil(*this).getVehicleInfo(); + vehicle_param_.width = vehicle_info.vehicle_width_m; + vehicle_param_.length = vehicle_info.vehicle_length_m; + vehicle_param_.wheelbase = vehicle_info.wheel_base_m; + vehicle_param_.rear_overhang = vehicle_info.rear_overhang_m; + vehicle_param_.front_overhang = vehicle_info.front_overhang_m; + } + + { // option parameter + is_publishing_debug_visualization_marker_ = + declare_parameter("option.is_publishing_debug_visualization_marker"); + is_publishing_clearance_map_ = declare_parameter("option.is_publishing_clearance_map"); + is_publishing_object_clearance_map_ = + declare_parameter("option.is_publishing_object_clearance_map"); + is_publishing_area_with_objects_ = + declare_parameter("option.is_publishing_area_with_objects"); + + is_showing_debug_info_ = declare_parameter("option.is_showing_debug_info"); + is_showing_calculation_time_ = declare_parameter("option.is_showing_calculation_time"); + is_stopping_if_outside_drivable_area_ = + declare_parameter("option.is_stopping_if_outside_drivable_area"); + + // drivability check + use_vehicle_circles_for_drivability_ = + declare_parameter("advanced.option.drivability_check.use_vehicle_circles"); + if (use_vehicle_circles_for_drivability_) { + // vehicle_circles + // NOTE: Vehicle shape for drivability check is considered as a set of circles + use_manual_vehicle_circles_for_drivability_ = declare_parameter( + "advanced.option.drivability_check.vehicle_circles.use_manual_vehicle_circles"); + vehicle_circle_constraints_num_for_drivability_ = declare_parameter( + "advanced.option.drivability_check.vehicle_circles.num_for_constraints"); + if (use_manual_vehicle_circles_for_drivability_) { // vehicle circles are designated manually + vehicle_circle_longitudinal_offsets_for_drivability_ = + declare_parameter>( + "advanced.option.drivability_check.vehicle_circles.longitudinal_offsets"); + vehicle_circle_radius_for_drivability_ = + declare_parameter("advanced.option.drivability_check.vehicle_circles.radius"); + } else { // vehicle circles are calculated automatically with designated ratio + const int default_radius_num = + std::round(vehicle_param_.length / vehicle_param_.width * 1.5); + + vehicle_circle_radius_num_for_drivability_ = declare_parameter( + "advanced.option.drivability_check.vehicle_circles.num_for_radius", default_radius_num); + vehicle_circle_radius_ratio_for_drivability_ = declare_parameter( + "advanced.option.drivability_check.vehicle_circles.radius_ratio"); + + std::tie( + vehicle_circle_radius_for_drivability_, + vehicle_circle_longitudinal_offsets_for_drivability_) = + calcVehicleCirclesInfo( + vehicle_param_, vehicle_circle_constraints_num_for_drivability_, + vehicle_circle_radius_num_for_drivability_, + vehicle_circle_radius_ratio_for_drivability_); + } + } + + enable_avoidance_ = declare_parameter("option.enable_avoidance"); + enable_pre_smoothing_ = declare_parameter("option.enable_pre_smoothing"); + skip_optimization_ = declare_parameter("option.skip_optimization"); + reset_prev_optimization_ = declare_parameter("option.reset_prev_optimization"); + } + + { // trajectory parameter + traj_param_ = TrajectoryParam{}; + + // common + traj_param_.num_sampling_points = declare_parameter("common.num_sampling_points"); + traj_param_.trajectory_length = declare_parameter("common.trajectory_length"); + traj_param_.forward_fixing_min_distance = + declare_parameter("common.forward_fixing_min_distance"); + traj_param_.forward_fixing_min_time = + declare_parameter("common.forward_fixing_min_time"); + traj_param_.backward_fixing_distance = + declare_parameter("common.backward_fixing_distance"); + traj_param_.delta_arc_length_for_trajectory = + declare_parameter("common.delta_arc_length_for_trajectory"); + + traj_param_.delta_dist_threshold_for_closest_point = + declare_parameter("common.delta_dist_threshold_for_closest_point"); + traj_param_.delta_yaw_threshold_for_closest_point = + declare_parameter("common.delta_yaw_threshold_for_closest_point"); + traj_param_.delta_yaw_threshold_for_straight = + declare_parameter("common.delta_yaw_threshold_for_straight"); + + traj_param_.num_fix_points_for_extending = + declare_parameter("common.num_fix_points_for_extending"); + traj_param_.max_dist_for_extending_end_point = + declare_parameter("common.max_dist_for_extending_end_point"); + + // object + traj_param_.max_avoiding_ego_velocity_ms = + declare_parameter("object.max_avoiding_ego_velocity_ms"); + traj_param_.max_avoiding_objects_velocity_ms = + declare_parameter("object.max_avoiding_objects_velocity_ms"); + traj_param_.is_avoiding_unknown = + declare_parameter("object.avoiding_object_type.unknown", true); + traj_param_.is_avoiding_car = declare_parameter("object.avoiding_object_type.car", true); + traj_param_.is_avoiding_truck = + declare_parameter("object.avoiding_object_type.truck", true); + traj_param_.is_avoiding_bus = declare_parameter("object.avoiding_object_type.bus", true); + traj_param_.is_avoiding_bicycle = + declare_parameter("object.avoiding_object_type.bicycle", true); + traj_param_.is_avoiding_motorbike = + declare_parameter("object.avoiding_object_type.motorbike", true); + traj_param_.is_avoiding_pedestrian = + declare_parameter("object.avoiding_object_type.pedestrian", true); + traj_param_.is_avoiding_animal = + declare_parameter("object.avoiding_object_type.animal", true); + } + + { // elastic band parameter + eb_param_ = EBParam{}; + + // common + eb_param_.num_joint_buffer_points = + declare_parameter("advanced.eb.common.num_joint_buffer_points"); + eb_param_.num_offset_for_begin_idx = + declare_parameter("advanced.eb.common.num_offset_for_begin_idx"); + eb_param_.delta_arc_length_for_eb = + declare_parameter("advanced.eb.common.delta_arc_length_for_eb"); + eb_param_.num_sampling_points_for_eb = + declare_parameter("advanced.eb.common.num_sampling_points_for_eb"); + + // clearance + eb_param_.clearance_for_straight_line = + declare_parameter("advanced.eb.clearance.clearance_for_straight_line"); + eb_param_.clearance_for_joint = + declare_parameter("advanced.eb.clearance.clearance_for_joint"); + eb_param_.clearance_for_only_smoothing = + declare_parameter("advanced.eb.clearance.clearance_for_only_smoothing"); + + // qp + eb_param_.qp_param.max_iteration = declare_parameter("advanced.eb.qp.max_iteration"); + eb_param_.qp_param.eps_abs = declare_parameter("advanced.eb.qp.eps_abs"); + eb_param_.qp_param.eps_rel = declare_parameter("advanced.eb.qp.eps_rel"); + + // other + eb_param_.clearance_for_fixing = 0.0; + } + + { // mpt param + mpt_param_ = MPTParam{}; + + // option + // TODO(murooka) implement plan_from_ego + mpt_param_.plan_from_ego = false; + // mpt_param_.plan_from_ego = declare_parameter("mpt.option.plan_from_ego"); + mpt_param_.steer_limit_constraint = + declare_parameter("mpt.option.steer_limit_constraint"); + mpt_param_.fix_points_around_ego = declare_parameter("mpt.option.fix_points_around_ego"); + mpt_param_.enable_warm_start = declare_parameter("mpt.option.enable_warm_start"); + mpt_param_.enable_manual_warm_start = + declare_parameter("mpt.option.enable_manual_warm_start"); + mpt_visualize_sampling_num_ = declare_parameter("mpt.option.visualize_sampling_num"); + + // common + mpt_param_.num_curvature_sampling_points = + declare_parameter("mpt.common.num_curvature_sampling_points"); + + mpt_param_.delta_arc_length_for_mpt_points = + declare_parameter("mpt.common.delta_arc_length_for_mpt_points"); + + // kinematics + mpt_param_.max_steer_rad = + declare_parameter("mpt.kinematics.max_steer_deg") * M_PI / 180.0; + + // By default, optimization_center_offset will be vehicle_info.wheel_base * 0.8 + // The 0.8 scale is adopted as it performed the best. + constexpr double default_wheelbase_ratio = 0.8; + mpt_param_.optimization_center_offset = declare_parameter( + "mpt.kinematics.optimization_center_offset", + vehicle_param_.wheelbase * default_wheelbase_ratio); + + // collision free constraints + mpt_param_.l_inf_norm = + declare_parameter("advanced.mpt.collision_free_constraints.option.l_inf_norm"); + mpt_param_.soft_constraint = + declare_parameter("advanced.mpt.collision_free_constraints.option.soft_constraint"); + mpt_param_.hard_constraint = + declare_parameter("advanced.mpt.collision_free_constraints.option.hard_constraint"); + // TODO(murooka) implement two-step soft constraint + mpt_param_.two_step_soft_constraint = false; + // mpt_param_.two_step_soft_constraint = + // declare_parameter("advanced.mpt.collision_free_constraints.option.two_step_soft_constraint"); + + // vehicle_circles + // NOTE: Vehicle shape for collision free constraints is considered as a set of circles + use_manual_vehicle_circles_for_mpt_ = declare_parameter( + "advanced.mpt.collision_free_constraints.vehicle_circles.use_manual_vehicle_circles"); + vehicle_circle_constraints_num_for_mpt_ = declare_parameter( + "advanced.mpt.collision_free_constraints.vehicle_circles.num_for_constraints"); + if (use_manual_vehicle_circles_for_mpt_) { // vehicle circles are designated manually + mpt_param_.vehicle_circle_longitudinal_offsets = declare_parameter>( + "advanced.mpt.collision_free_constraints.vehicle_circles.longitudinal_offsets"); + mpt_param_.vehicle_circle_radius = + declare_parameter("advanced.mpt.collision_free_constraints.vehicle_circles.radius"); + } else { // vehicle circles are calculated automatically with designated ratio + const int default_radius_num = std::round(vehicle_param_.length / vehicle_param_.width * 1.5); + + vehicle_circle_radius_num_for_mpt_ = declare_parameter( + "advanced.mpt.collision_free_constraints.vehicle_circles.num_for_radius", + default_radius_num); + vehicle_circle_radius_ratio_for_mpt_ = declare_parameter( + "advanced.mpt.collision_free_constraints.vehicle_circles.radius_ratio"); + + std::tie(mpt_param_.vehicle_circle_radius, mpt_param_.vehicle_circle_longitudinal_offsets) = + calcVehicleCirclesInfo( + vehicle_param_, vehicle_circle_constraints_num_for_mpt_, + vehicle_circle_radius_num_for_mpt_, vehicle_circle_radius_ratio_for_mpt_); + } + + // clearance + mpt_param_.hard_clearance_from_road = + declare_parameter("advanced.mpt.clearance.hard_clearance_from_road"); + mpt_param_.soft_clearance_from_road = + declare_parameter("advanced.mpt.clearance.soft_clearance_from_road"); + mpt_param_.soft_second_clearance_from_road = + declare_parameter("advanced.mpt.clearance.soft_second_clearance_from_road"); + mpt_param_.extra_desired_clearance_from_road = + declare_parameter("advanced.mpt.clearance.extra_desired_clearance_from_road"); + mpt_param_.clearance_from_object = + declare_parameter("advanced.mpt.clearance.clearance_from_object"); + + // weight + mpt_param_.soft_avoidance_weight = + declare_parameter("advanced.mpt.weight.soft_avoidance_weight"); + mpt_param_.soft_second_avoidance_weight = + declare_parameter("advanced.mpt.weight.soft_second_avoidance_weight"); + + mpt_param_.lat_error_weight = declare_parameter("advanced.mpt.weight.lat_error_weight"); + mpt_param_.yaw_error_weight = declare_parameter("advanced.mpt.weight.yaw_error_weight"); + mpt_param_.yaw_error_rate_weight = + declare_parameter("advanced.mpt.weight.yaw_error_rate_weight"); + mpt_param_.steer_input_weight = + declare_parameter("advanced.mpt.weight.steer_input_weight"); + mpt_param_.steer_rate_weight = + declare_parameter("advanced.mpt.weight.steer_rate_weight"); + + mpt_param_.obstacle_avoid_lat_error_weight = + declare_parameter("advanced.mpt.weight.obstacle_avoid_lat_error_weight"); + mpt_param_.obstacle_avoid_yaw_error_weight = + declare_parameter("advanced.mpt.weight.obstacle_avoid_yaw_error_weight"); + mpt_param_.obstacle_avoid_steer_input_weight = + declare_parameter("advanced.mpt.weight.obstacle_avoid_steer_input_weight"); + mpt_param_.near_objects_length = + declare_parameter("advanced.mpt.weight.near_objects_length"); + + mpt_param_.terminal_lat_error_weight = + declare_parameter("advanced.mpt.weight.terminal_lat_error_weight"); + mpt_param_.terminal_yaw_error_weight = + declare_parameter("advanced.mpt.weight.terminal_yaw_error_weight"); + mpt_param_.terminal_path_lat_error_weight = + declare_parameter("advanced.mpt.weight.terminal_path_lat_error_weight"); + mpt_param_.terminal_path_yaw_error_weight = + declare_parameter("advanced.mpt.weight.terminal_path_yaw_error_weight"); + } + + { // replan + max_path_shape_change_dist_for_replan_ = + declare_parameter("replan.max_path_shape_change_dist"); + max_ego_moving_dist_for_replan_ = + declare_parameter("replan.max_ego_moving_dist_for_replan"); + max_delta_time_sec_for_replan_ = + declare_parameter("replan.max_delta_time_sec_for_replan"); + } + + // TODO(murooka) tune this param when avoiding with obstacle_avoidance_planner + traj_param_.center_line_width = vehicle_param_.width; + + objects_ptr_ = std::make_unique(); // set parameter callback set_param_res_ = this->add_on_set_parameters_callback( std::bind(&ObstacleAvoidancePlanner::paramCallback, this, std::placeholders::_1)); - initialize(); -} + resetPlanning(); -ObstacleAvoidancePlanner::~ObstacleAvoidancePlanner() {} + self_pose_listener_.waitForFirstPose(); +} rcl_interfaces::msg::SetParametersResult ObstacleAvoidancePlanner::paramCallback( const std::vector & parameters) { - auto update_param = [&](const std::string & name, double & v) { - auto it = std::find_if( - parameters.cbegin(), parameters.cend(), - [&name](const rclcpp::Parameter & parameter) { return parameter.get_name() == name; }); - if (it != parameters.cend()) { - v = it->as_double(); - return true; + using tier4_autoware_utils::updateParam; + + { // option parameter + updateParam( + parameters, "option.is_publishing_debug_visualization_marker", + is_publishing_debug_visualization_marker_); + updateParam( + parameters, "option.is_publishing_clearance_map", is_publishing_clearance_map_); + updateParam( + parameters, "option.is_publishing_object_clearance_map", is_publishing_object_clearance_map_); + updateParam( + parameters, "option.is_publishing_area_with_objects", is_publishing_area_with_objects_); + + updateParam(parameters, "option.is_showing_debug_info", is_showing_debug_info_); + updateParam( + parameters, "option.is_showing_calculation_time", is_showing_calculation_time_); + updateParam( + parameters, "option.is_stopping_if_outside_drivable_area", + is_stopping_if_outside_drivable_area_); + + // drivability check + updateParam( + parameters, "advanced.option.drivability_check.use_vehicle_circles", + use_vehicle_circles_for_drivability_); + if (use_vehicle_circles_for_drivability_) { + updateParam( + parameters, "advanced.option.drivability_check.vehicle_circles.use_manual_vehicle_circles", + use_manual_vehicle_circles_for_drivability_); + updateParam( + parameters, "advanced.option.drivability_check.vehicle_circles.num_for_constraints", + vehicle_circle_constraints_num_for_drivability_); + if (use_manual_vehicle_circles_for_drivability_) { + updateParam>( + parameters, "advanced.option.drivability_check.vehicle_circles.longitudinal_offsets", + vehicle_circle_longitudinal_offsets_for_drivability_); + updateParam( + parameters, "advanced.option.drivability_check.vehicle_circles.radius", + vehicle_circle_radius_for_drivability_); + } else { + updateParam( + parameters, "advanced.option.drivability_check.vehicle_circles.num_for_radius", + vehicle_circle_radius_num_for_drivability_); + updateParam( + parameters, "advanced.option.drivability_check.vehicle_circles.radius_ratio", + vehicle_circle_radius_ratio_for_drivability_); + + std::tie( + vehicle_circle_radius_for_drivability_, + vehicle_circle_longitudinal_offsets_for_drivability_) = + calcVehicleCirclesInfo( + vehicle_param_, vehicle_circle_constraints_num_for_drivability_, + vehicle_circle_radius_num_for_drivability_, + vehicle_circle_radius_ratio_for_drivability_); + } } - return false; - }; - - // trajectory total/fixing length - update_param("trajectory_length", traj_param_->trajectory_length); - update_param("forward_fixing_distance", traj_param_->forward_fixing_distance); - update_param("backward_fixing_distance", traj_param_->backward_fixing_distance); - - // clearance for unique points - update_param("clearance_for_straight_line", constrain_param_->clearance_for_straight_line); - update_param("clearance_for_joint", constrain_param_->clearance_for_joint); - update_param("clearance_for_only_smoothing", constrain_param_->clearance_for_only_smoothing); - update_param( - "clearance_from_object_for_straight", constrain_param_->clearance_from_object_for_straight); - - // clearance(distance) when generating trajectory - update_param("clearance_from_road", constrain_param_->clearance_from_road); - update_param("clearance_from_object", constrain_param_->clearance_from_object); - update_param("min_object_clearance_for_joint", constrain_param_->min_object_clearance_for_joint); - update_param( - "extra_desired_clearance_from_road", constrain_param_->extra_desired_clearance_from_road); - - // avoiding param - update_param("max_avoiding_objects_velocity_ms", traj_param_->max_avoiding_objects_velocity_ms); - update_param("max_avoiding_ego_velocity_ms", traj_param_->max_avoiding_ego_velocity_ms); - update_param("center_line_width", traj_param_->center_line_width); - update_param( - "acceleration_for_non_deceleration_range", - traj_param_->acceleration_for_non_deceleration_range); - - // mpt param - update_param("base_point_weight", mpt_param_->base_point_weight); - update_param("top_point_weight", mpt_param_->top_point_weight); - update_param("mid_point_weight", mpt_param_->mid_point_weight); - update_param("lat_error_weight", mpt_param_->lat_error_weight); - update_param("yaw_error_weight", mpt_param_->yaw_error_weight); - update_param("steer_input_weight", mpt_param_->steer_input_weight); - update_param("steer_rate_weight", mpt_param_->steer_rate_weight); - update_param("steer_acc_weight", mpt_param_->steer_acc_weight); + + updateParam(parameters, "option.enable_avoidance", enable_avoidance_); + updateParam(parameters, "option.enable_pre_smoothing", enable_pre_smoothing_); + updateParam(parameters, "option.skip_optimization", skip_optimization_); + updateParam(parameters, "option.reset_prev_optimization", reset_prev_optimization_); + } + + { // trajectory parameter + // common + updateParam(parameters, "common.num_sampling_points", traj_param_.num_sampling_points); + updateParam(parameters, "common.trajectory_length", traj_param_.trajectory_length); + updateParam( + parameters, "common.forward_fixing_min_distance", traj_param_.forward_fixing_min_distance); + updateParam( + parameters, "common.forward_fixing_min_time", traj_param_.forward_fixing_min_time); + updateParam( + parameters, "common.backward_fixing_distance", traj_param_.backward_fixing_distance); + updateParam( + parameters, "common.delta_arc_length_for_trajectory", + traj_param_.delta_arc_length_for_trajectory); + + updateParam( + parameters, "common.delta_dist_threshold_for_closest_point", + traj_param_.delta_dist_threshold_for_closest_point); + updateParam( + parameters, "common.delta_yaw_threshold_for_closest_point", + traj_param_.delta_yaw_threshold_for_closest_point); + updateParam( + parameters, "common.delta_yaw_threshold_for_straight", + traj_param_.delta_yaw_threshold_for_straight); + updateParam( + parameters, "common.num_fix_points_for_extending", traj_param_.num_fix_points_for_extending); + updateParam( + parameters, "common.max_dist_for_extending_end_point", + traj_param_.max_dist_for_extending_end_point); + + // object + updateParam( + parameters, "object.max_avoiding_ego_velocity_ms", traj_param_.max_avoiding_ego_velocity_ms); + updateParam( + parameters, "object.max_avoiding_objects_velocity_ms", + traj_param_.max_avoiding_objects_velocity_ms); + updateParam( + parameters, "object.avoiding_object_type.unknown", traj_param_.is_avoiding_unknown); + updateParam(parameters, "object.avoiding_object_type.car", traj_param_.is_avoiding_car); + updateParam( + parameters, "object.avoiding_object_type.truck", traj_param_.is_avoiding_truck); + updateParam(parameters, "object.avoiding_object_type.bus", traj_param_.is_avoiding_bus); + updateParam( + parameters, "object.avoiding_object_type.bicycle", traj_param_.is_avoiding_bicycle); + updateParam( + parameters, "object.avoiding_object_type.motorbike", traj_param_.is_avoiding_motorbike); + updateParam( + parameters, "object.avoiding_object_type.pedestrian", traj_param_.is_avoiding_pedestrian); + updateParam( + parameters, "object.avoiding_object_type.animal", traj_param_.is_avoiding_animal); + } + + { // elastic band parameter + // common + updateParam( + parameters, "advanced.eb.common.num_joint_buffer_points", eb_param_.num_joint_buffer_points); + updateParam( + parameters, "advanced.eb.common.num_offset_for_begin_idx", + eb_param_.num_offset_for_begin_idx); + updateParam( + parameters, "advanced.eb.common.delta_arc_length_for_eb", eb_param_.delta_arc_length_for_eb); + updateParam( + parameters, "advanced.eb.common.num_sampling_points_for_eb", + eb_param_.num_sampling_points_for_eb); + + // clearance + updateParam( + parameters, "advanced.eb.clearance.clearance_for_straight_line", + eb_param_.clearance_for_straight_line); + updateParam( + parameters, "advanced.eb.clearance.clearance_for_joint", eb_param_.clearance_for_joint); + updateParam( + parameters, "advanced.eb.clearance.clearance_for_only_smoothing", + eb_param_.clearance_for_only_smoothing); + + // qp + updateParam(parameters, "advanced.eb.qp.max_iteration", eb_param_.qp_param.max_iteration); + updateParam(parameters, "advanced.eb.qp.eps_abs", eb_param_.qp_param.eps_abs); + updateParam(parameters, "advanced.eb.qp.eps_rel", eb_param_.qp_param.eps_rel); + } + + { // mpt param + // option + // updateParam(parameters, "mpt.option.plan_from_ego", mpt_param_.plan_from_ego); + updateParam( + parameters, "mpt.option.steer_limit_constraint", mpt_param_.steer_limit_constraint); + updateParam( + parameters, "mpt.option.fix_points_around_ego", mpt_param_.fix_points_around_ego); + updateParam(parameters, "mpt.option.enable_warm_start", mpt_param_.enable_warm_start); + updateParam( + parameters, "mpt.option.enable_manual_warm_start", mpt_param_.enable_manual_warm_start); + updateParam(parameters, "mpt.option.visualize_sampling_num", mpt_visualize_sampling_num_); + + // common + updateParam( + parameters, "mpt.common.num_curvature_sampling_points", + mpt_param_.num_curvature_sampling_points); + + updateParam( + parameters, "mpt.common.delta_arc_length_for_mpt_points", + mpt_param_.delta_arc_length_for_mpt_points); + + // kinematics + double max_steer_deg = mpt_param_.max_steer_rad * 180.0 / M_PI; + updateParam(parameters, "mpt.kinematics.max_steer_deg", max_steer_deg); + mpt_param_.max_steer_rad = max_steer_deg * M_PI / 180.0; + updateParam( + parameters, "mpt.kinematics.optimization_center_offset", + mpt_param_.optimization_center_offset); + + // collision_free_constraints + updateParam( + parameters, "advanced.mpt.collision_free_constraints.option.l_inf_norm", + mpt_param_.l_inf_norm); + // updateParam( + // parameters, "advanced.mpt.collision_free_constraints.option.two_step_soft_constraint", + // mpt_param_.two_step_soft_constraint); + updateParam( + parameters, "advanced.mpt.collision_free_constraints.option.soft_constraint", + mpt_param_.soft_constraint); + updateParam( + parameters, "advanced.mpt.collision_free_constraints.option.hard_constraint", + mpt_param_.hard_constraint); + + // vehicle_circles + updateParam( + parameters, + "advanced.mpt.collision_free_constraints.vehicle_circles.use_manual_vehicle_circles", + use_manual_vehicle_circles_for_mpt_); + updateParam( + parameters, "advanced.mpt.collision_free_constraints.vehicle_circles.num_for_constraints", + vehicle_circle_constraints_num_for_mpt_); + if (use_manual_vehicle_circles_for_mpt_) { + updateParam>( + parameters, "advanced.mpt.collision_free_constraints.vehicle_circles.longitudinal_offsets", + mpt_param_.vehicle_circle_longitudinal_offsets); + updateParam( + parameters, "advanced.mpt.collision_free_constraints.vehicle_circles.radius", + mpt_param_.vehicle_circle_radius); + } else { // vehicle circles are calculated automatically with designated ratio + updateParam( + parameters, "advanced.mpt.collision_free_constraints.vehicle_circles.num_for_radius", + vehicle_circle_radius_num_for_mpt_); + updateParam( + parameters, "advanced.mpt.collision_free_constraints.vehicle_circles.radius_ratio", + vehicle_circle_radius_ratio_for_mpt_); + + std::tie(mpt_param_.vehicle_circle_radius, mpt_param_.vehicle_circle_longitudinal_offsets) = + calcVehicleCirclesInfo( + vehicle_param_, vehicle_circle_constraints_num_for_mpt_, + vehicle_circle_radius_num_for_mpt_, vehicle_circle_radius_ratio_for_mpt_); + } + + // clearance + updateParam( + parameters, "advanced.mpt.clearance.hard_clearance_from_road", + mpt_param_.hard_clearance_from_road); + updateParam( + parameters, "advanced.mpt.clearance.soft_clearance_from_road", + mpt_param_.soft_clearance_from_road); + updateParam( + parameters, "advanced.mpt.clearance.soft_second_clearance_from_road", + mpt_param_.soft_second_clearance_from_road); + updateParam( + parameters, "advanced.mpt.clearance.extra_desired_clearance_from_road", + mpt_param_.extra_desired_clearance_from_road); + updateParam( + parameters, "advanced.mpt.clearance.clearance_from_object", mpt_param_.clearance_from_object); + + // weight + updateParam( + parameters, "advanced.mpt.weight.soft_avoidance_weight", mpt_param_.soft_avoidance_weight); + updateParam( + parameters, "advanced.mpt.weight.soft_second_avoidance_weight", + mpt_param_.soft_second_avoidance_weight); + + updateParam( + parameters, "advanced.mpt.weight.lat_error_weight", mpt_param_.lat_error_weight); + updateParam( + parameters, "advanced.mpt.weight.yaw_error_weight", mpt_param_.yaw_error_weight); + updateParam( + parameters, "advanced.mpt.weight.yaw_error_rate_weight", mpt_param_.yaw_error_rate_weight); + updateParam( + parameters, "advanced.mpt.weight.steer_input_weight", mpt_param_.steer_input_weight); + updateParam( + parameters, "advanced.mpt.weight.steer_rate_weight", mpt_param_.steer_rate_weight); + + updateParam( + parameters, "advanced.mpt.weight.obstacle_avoid_lat_error_weight", + mpt_param_.obstacle_avoid_lat_error_weight); + updateParam( + parameters, "advanced.mpt.weight.obstacle_avoid_yaw_error_weight", + mpt_param_.obstacle_avoid_yaw_error_weight); + updateParam( + parameters, "advanced.mpt.weight.obstacle_avoid_steer_input_weight", + mpt_param_.obstacle_avoid_steer_input_weight); + updateParam( + parameters, "advanced.mpt.weight.near_objects_length", mpt_param_.near_objects_length); + + updateParam( + parameters, "advanced.mpt.weight.terminal_lat_error_weight", + mpt_param_.terminal_lat_error_weight); + updateParam( + parameters, "advanced.mpt.weight.terminal_yaw_error_weight", + mpt_param_.terminal_yaw_error_weight); + updateParam( + parameters, "advanced.mpt.weight.terminal_path_lat_error_weight", + mpt_param_.terminal_path_lat_error_weight); + updateParam( + parameters, "advanced.mpt.weight.terminal_path_yaw_error_weight", + mpt_param_.terminal_path_yaw_error_weight); + } + + { // replan + updateParam( + parameters, "replan.max_path_shape_change_dist", max_path_shape_change_dist_for_replan_); + updateParam( + parameters, "replan.max_ego_moving_dist_for_replan", max_ego_moving_dist_for_replan_); + updateParam( + parameters, "replan.max_delta_time_sec_for_replan", max_delta_time_sec_for_replan_); + } + + resetPlanning(); rcl_interfaces::msg::SetParametersResult result; result.successful = true; @@ -303,21 +807,6 @@ rcl_interfaces::msg::SetParametersResult ObstacleAvoidancePlanner::paramCallback return result; } -// ROS callback functions -void ObstacleAvoidancePlanner::pathCallback( - const autoware_auto_planning_msgs::msg::Path::SharedPtr msg) -{ - std::lock_guard lock(mutex_); - current_ego_pose_ptr_ = getCurrentEgoPose(); - if ( - msg->points.empty() || msg->drivable_area.data.empty() || !current_ego_pose_ptr_ || - !current_twist_ptr_) { - return; - } - autoware_auto_planning_msgs::msg::Trajectory output_trajectory_msg = generateTrajectory(*msg); - trajectory_pub_->publish(output_trajectory_msg); -} - void ObstacleAvoidancePlanner::odomCallback(const nav_msgs::msg::Odometry::SharedPtr msg) { current_twist_ptr_ = std::make_unique(); @@ -328,7 +817,7 @@ void ObstacleAvoidancePlanner::odomCallback(const nav_msgs::msg::Odometry::Share void ObstacleAvoidancePlanner::objectsCallback( const autoware_auto_perception_msgs::msg::PredictedObjects::SharedPtr msg) { - in_objects_ptr_ = std::make_unique(*msg); + objects_ptr_ = std::make_unique(*msg); } void ObstacleAvoidancePlanner::enableAvoidanceCallback( @@ -336,508 +825,617 @@ void ObstacleAvoidancePlanner::enableAvoidanceCallback( { enable_avoidance_ = msg->enable_avoidance; } -// End ROS callback functions -autoware_auto_planning_msgs::msg::Trajectory ObstacleAvoidancePlanner::generateTrajectory( - const autoware_auto_planning_msgs::msg::Path & path) +void ObstacleAvoidancePlanner::resetPlanning() { - auto t_start = std::chrono::high_resolution_clock::now(); + RCLCPP_WARN(get_logger(), "[ObstacleAvoidancePlanner] Reset planning"); - const auto traj_points = generateOptimizedTrajectory(*current_ego_pose_ptr_, path); + costmap_generator_ptr_ = std::make_unique(); - const auto post_processed_traj = - generatePostProcessedTrajectory(*current_ego_pose_ptr_, path.points, traj_points); + eb_path_optimizer_ptr_ = std::make_unique( + is_showing_debug_info_, traj_param_, eb_param_, vehicle_param_); - auto output = tier4_autoware_utils::convertToTrajectory(post_processed_traj); - output.header = path.header; + mpt_optimizer_ptr_ = + std::make_unique(is_showing_debug_info_, traj_param_, vehicle_param_, mpt_param_); - prev_path_points_ptr_ = - std::make_unique>(path.points); - - auto t_end = std::chrono::high_resolution_clock::now(); - float elapsed_ms = std::chrono::duration(t_end - t_start).count(); - RCLCPP_INFO_EXPRESSION( - get_logger(), is_showing_debug_info_, - "Total time: = %f [ms]\n==========================", elapsed_ms); - return output; + prev_path_points_ptr_ = nullptr; + resetPrevOptimization(); } -std::vector -ObstacleAvoidancePlanner::generateOptimizedTrajectory( - const geometry_msgs::msg::Pose & ego_pose, const autoware_auto_planning_msgs::msg::Path & path) +void ObstacleAvoidancePlanner::resetPrevOptimization() { - if (!needReplan( - ego_pose, prev_ego_pose_ptr_, path.points, prev_replanned_time_ptr_, prev_path_points_ptr_, - prev_trajectories_ptr_)) { - return getPrevTrajectory(path.points); - } - prev_ego_pose_ptr_ = std::make_unique(ego_pose); - prev_replanned_time_ptr_ = std::make_unique(this->now()); - - DebugData debug_data; - const auto optional_trajs = eb_path_optimizer_ptr_->generateOptimizedTrajectory( - enable_avoidance_, ego_pose, path, prev_trajectories_ptr_, in_objects_ptr_->objects, - &debug_data); - if (!optional_trajs) { - RCLCPP_WARN_THROTTLE( - get_logger(), *get_clock(), std::chrono::milliseconds(1000).count(), - "[Avoidance] Optimization failed, passing previous trajectory"); - const bool is_prev_traj = true; - const auto prev_trajs_inside_area = calcTrajectoryInsideArea( - getPrevTrajs(path.points), path.points, debug_data.clearance_map, path.drivable_area.info, - &debug_data, is_prev_traj); - prev_trajectories_ptr_ = std::make_unique( - makePrevTrajectories(*current_ego_pose_ptr_, path.points, prev_trajs_inside_area.get())); - - const auto prev_traj = util::concatTraj(prev_trajs_inside_area.get()); - publishingDebugData(debug_data, path, prev_traj, *vehicle_param_); - return prev_traj; - } - - const auto trajs_inside_area = getTrajectoryInsideArea( - optional_trajs.get(), path.points, debug_data.clearance_map, path.drivable_area.info, - &debug_data); - - prev_trajectories_ptr_ = std::make_unique( - makePrevTrajectories(*current_ego_pose_ptr_, path.points, trajs_inside_area)); - const auto optimized_trajectory = util::concatTraj(trajs_inside_area); - publishingDebugData(debug_data, path, optimized_trajectory, *vehicle_param_); - return optimized_trajectory; + prev_optimal_trajs_ptr_ = nullptr; + eb_solved_count_ = 0; } -void ObstacleAvoidancePlanner::initialize() +void ObstacleAvoidancePlanner::pathCallback( + const autoware_auto_planning_msgs::msg::Path::SharedPtr path_ptr) { - RCLCPP_WARN(get_logger(), "[ObstacleAvoidancePlanner] Resetting"); - eb_path_optimizer_ptr_ = std::make_unique( - is_showing_debug_info_, *qp_param_, *traj_param_, *constrain_param_, *vehicle_param_, - *mpt_param_); - prev_path_points_ptr_ = nullptr; - prev_trajectories_ptr_ = nullptr; -} + stop_watch_.tic(__func__); -std::unique_ptr ObstacleAvoidancePlanner::getCurrentEgoPose() -{ - geometry_msgs::msg::TransformStamped tf_current_pose; - - try { - tf_current_pose = tf_buffer_ptr_->lookupTransform( - "map", "base_link", rclcpp::Time(0), rclcpp::Duration::from_seconds(1.0)); - } catch (tf2::TransformException & ex) { - RCLCPP_ERROR(get_logger(), "[ObstacleAvoidancePlanner] %s", ex.what()); - return nullptr; - } - - geometry_msgs::msg::Pose p; - p.orientation = tf_current_pose.transform.rotation; - p.position.x = tf_current_pose.transform.translation.x; - p.position.y = tf_current_pose.transform.translation.y; - p.position.z = tf_current_pose.transform.translation.z; - std::unique_ptr p_ptr = std::make_unique(p); - return p_ptr; + if (path_ptr->points.empty() || path_ptr->drivable_area.data.empty() || !current_twist_ptr_) { + return; + } + + current_ego_pose_ = self_pose_listener_.getCurrentPose()->pose; + debug_data_ptr_ = std::make_shared(); + debug_data_ptr_->init( + is_showing_calculation_time_, mpt_visualize_sampling_num_, current_ego_pose_, + mpt_param_.vehicle_circle_radius, mpt_param_.vehicle_circle_longitudinal_offsets); + + // generate optimized trajectory + const auto optimized_traj_points = generateOptimizedTrajectory(*path_ptr); + + // generate post processed trajectory + const auto post_processed_traj_points = + generatePostProcessedTrajectory(path_ptr->points, optimized_traj_points); + + // convert to output msg type + auto output_traj_msg = tier4_autoware_utils::convertToTrajectory(post_processed_traj_points); + output_traj_msg.header = path_ptr->header; + + // publish debug data + publishDebugDataInMain(*path_ptr); + + { // print and publish debug msg + debug_data_ptr_->msg_stream << __func__ << ":= " << stop_watch_.toc(__func__) << " [ms]\n" + << "========================================"; + tier4_debug_msgs::msg::StringStamped debug_msg_msg; + debug_msg_msg.stamp = get_clock()->now(); + debug_msg_msg.data = debug_data_ptr_->msg_stream.getString(); + debug_msg_pub_->publish(debug_msg_msg); + } + + // make previous variables + prev_path_points_ptr_ = + std::make_unique>(path_ptr->points); + prev_ego_pose_ptr_ = std::make_unique(current_ego_pose_); + + traj_pub_->publish(output_traj_msg); } std::vector -ObstacleAvoidancePlanner::generatePostProcessedTrajectory( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const std::vector & optimized_points) const +ObstacleAvoidancePlanner::generateOptimizedTrajectory( + const autoware_auto_planning_msgs::msg::Path & path) { - auto t_start = std::chrono::high_resolution_clock::now(); + stop_watch_.tic(__func__); - std::vector trajectory_points; - if (path_points.empty()) { - autoware_auto_planning_msgs::msg::TrajectoryPoint tmp_point; - tmp_point.pose = ego_pose; - tmp_point.longitudinal_velocity_mps = 0; - trajectory_points.push_back(tmp_point); - return trajectory_points; + if (reset_prev_optimization_) { + resetPrevOptimization(); } - if (optimized_points.empty()) { - trajectory_points = util::convertPathToTrajectory(path_points); - return trajectory_points; + + // return prev trajectory if replan is not required + if (!checkReplan(path.points)) { + if (prev_optimal_trajs_ptr_) { + return prev_optimal_trajs_ptr_->model_predictive_trajectory; + } + + return points_utils::convertToTrajectoryPoints(path.points); } - trajectory_points = convertPointsToTrajectory(path_points, optimized_points); + latest_replanned_time_ptr_ = std::make_unique(this->now()); + + // create clearance maps + const CVMaps cv_maps = costmap_generator_ptr_->getMaps( + enable_avoidance_, path, objects_ptr_->objects, traj_param_, debug_data_ptr_); + + // calculate trajectory with EB and MPT + auto optimal_trajs = optimizeTrajectory(path, cv_maps); + + // insert 0 velocity when trajectory is over drivable area + if (is_stopping_if_outside_drivable_area_) { + insertZeroVelocityOutsideDrivableArea(optimal_trajs.model_predictive_trajectory, cv_maps); + } + + publishDebugDataInOptimization(path, optimal_trajs.model_predictive_trajectory); - auto t_end = std::chrono::high_resolution_clock::now(); - float elapsed_ms = std::chrono::duration(t_end - t_start).count(); - RCLCPP_INFO_EXPRESSION( - get_logger(), is_showing_debug_info_, "Post processing time: = %f [ms]", elapsed_ms); + // make previous trajectories + prev_optimal_trajs_ptr_ = + std::make_unique(makePrevTrajectories(path.points, optimal_trajs)); - return trajectory_points; + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return optimal_trajs.model_predictive_trajectory; } -bool ObstacleAvoidancePlanner::needReplan( - const geometry_msgs::msg::Pose & ego_pose, - const std::unique_ptr & prev_ego_pose, - const std::vector & path_points, - const std::unique_ptr & prev_replanned_time, - const std::unique_ptr> & - prev_path_points, - std::unique_ptr & prev_trajs) +// check if optimization is required or not. +// NOTE: previous trajectories information will be reset as well in some cases. +bool ObstacleAvoidancePlanner::checkReplan( + const std::vector & path_points) { - if (!prev_ego_pose || !prev_replanned_time || !prev_path_points || !prev_trajs) { + if ( + !prev_ego_pose_ptr_ || !latest_replanned_time_ptr_ || !prev_path_points_ptr_ || + !prev_optimal_trajs_ptr_) { + return true; + } + + const double max_mpt_length = + traj_param_.num_sampling_points * mpt_param_.delta_arc_length_for_mpt_points; + if (isPathShapeChanged( + current_ego_pose_, path_points, prev_path_points_ptr_, max_mpt_length, + max_path_shape_change_dist_for_replan_, + traj_param_.delta_yaw_threshold_for_closest_point)) { + RCLCPP_INFO(get_logger(), "Replan since path shape was changed."); return true; } - if (isPathShapeChanged(ego_pose, path_points, prev_path_points)) { - RCLCPP_INFO(get_logger(), "[Avoidance] Path shape is changed, reset prev trajs"); - prev_trajs = nullptr; + if (isPathGoalChanged(current_twist_ptr_->twist.linear.x, path_points, prev_path_points_ptr_)) { + RCLCPP_INFO(get_logger(), "Replan with resetting optimization since path goal was changed."); + resetPrevOptimization(); return true; } - if (!util::hasValidNearestPointFromEgo( - *current_ego_pose_ptr_, *prev_trajectories_ptr_, *traj_param_)) { + // For when ego pose is lost or new ego pose is designated in simulation + const double delta_dist = + tier4_autoware_utils::calcDistance2d(current_ego_pose_.position, prev_ego_pose_ptr_->position); + if (delta_dist > max_ego_moving_dist_for_replan_) { RCLCPP_INFO( - get_logger(), "[Avoidance] Could not find valid nearest point from ego, reset prev trajs"); - prev_trajs = nullptr; + get_logger(), + "Replan with resetting optimization since current ego pose is far from previous ego pose."); + resetPrevOptimization(); return true; } - const double delta_dist = util::calculate2DDistance(ego_pose.position, prev_ego_pose->position); - if (delta_dist > min_delta_dist_for_replan_) { + // For when ego pose moves far from trajectory + if (!hasValidNearestPointFromEgo(current_ego_pose_, *prev_optimal_trajs_ptr_, traj_param_)) { + RCLCPP_INFO( + get_logger(), + "Replan with resetting optimization since valid nearest trajectory point from ego was not " + "found."); + resetPrevOptimization(); return true; } - rclcpp::Duration delta_time = this->now() - *prev_replanned_time; - const double delta_time_sec = delta_time.seconds(); - if (delta_time_sec > min_delta_time_sec_for_replan_) { + const double delta_time_sec = (this->now() - *latest_replanned_time_ptr_).seconds(); + if (delta_time_sec > max_delta_time_sec_for_replan_) { return true; } return false; } -bool ObstacleAvoidancePlanner::isPathShapeChanged( - const geometry_msgs::msg::Pose & ego_pose, - const std::vector & path_points, - const std::unique_ptr> & - prev_path_points) +Trajectories ObstacleAvoidancePlanner::optimizeTrajectory( + const autoware_auto_planning_msgs::msg::Path & path, const CVMaps & cv_maps) { - if (!prev_path_points) { - return true; + stop_watch_.tic(__func__); + + if (skip_optimization_) { + const auto traj = points_utils::convertToTrajectoryPoints(path.points); + Trajectories trajs; + trajs.smoothed_trajectory = traj; + trajs.model_predictive_trajectory = traj; + return trajs; } - const int default_nearest_prev_path_idx = 0; - const int nearest_prev_path_idx = util::getNearestIdx( - *prev_path_points, ego_pose, default_nearest_prev_path_idx, - traj_param_->delta_yaw_threshold_for_closest_point); - const int default_nearest_path_idx = 0; - const int nearest_path_idx = util::getNearestIdx( - path_points, ego_pose, default_nearest_path_idx, - traj_param_->delta_yaw_threshold_for_closest_point); - const auto prev_first = prev_path_points->begin() + nearest_prev_path_idx; - const auto prev_last = prev_path_points->end(); - std::vector truncated_prev_points( - prev_first, prev_last); + // EB: smooth trajectory if enable_pre_smoothing is true + const auto eb_traj = + [&]() -> boost::optional> { + if (enable_pre_smoothing_) { + return eb_path_optimizer_ptr_->getEBTrajectory( + current_ego_pose_, path, prev_optimal_trajs_ptr_, current_twist_ptr_->twist.linear.x, + debug_data_ptr_); + } + return points_utils::convertToTrajectoryPoints(path.points); + }(); + if (!eb_traj) { + return getPrevTrajs(path.points); + } - const auto first = path_points.begin() + nearest_path_idx; - const auto last = path_points.end(); - std::vector truncated_points(first, last); + // EB has to be solved twice before solving MPT with fixed points + // since the result of EB is likely to change with/without fixing (1st/2nd EB) + // that makes MPT fixing points worse. + if (eb_solved_count_ < 2) { + eb_solved_count_++; - for (const auto & prev_point : truncated_prev_points) { - double min_dist = std::numeric_limits::max(); - for (const auto & point : truncated_points) { - const double dist = util::calculate2DDistance(point.pose.position, prev_point.pose.position); - if (dist < min_dist) { - min_dist = dist; - } - } - if (min_dist > distance_for_path_shape_change_detection_) { - return true; + if (prev_optimal_trajs_ptr_) { + prev_optimal_trajs_ptr_->model_predictive_trajectory.clear(); + prev_optimal_trajs_ptr_->mpt_ref_points.clear(); } } - return false; -} - -std::vector -ObstacleAvoidancePlanner::convertPointsToTrajectory( - const std::vector & path_points, - const std::vector & trajectory_points) const -{ - std::vector interpolated_points = - util::getInterpolatedPoints(trajectory_points, traj_param_->delta_arc_length_for_trajectory); - // add discarded point in the process of interpolation - interpolated_points.push_back(trajectory_points.back().pose.position); - if (static_cast(interpolated_points.size()) < min_num_points_for_getting_yaw_) { - return util::convertPathToTrajectory(path_points); - } - std::vector candidate_points = interpolated_points; - geometry_msgs::msg::Pose last_pose; - last_pose.position = candidate_points.back(); - last_pose.orientation = util::getQuaternionFromPoints( - candidate_points.back(), candidate_points[candidate_points.size() - 2]); - const auto extended_point_opt = util::getLastExtendedPoint( - path_points.back(), last_pose, traj_param_->delta_yaw_threshold_for_closest_point, - traj_param_->max_dist_for_extending_end_point); - if (extended_point_opt) { - candidate_points.push_back(extended_point_opt.get()); - } - - const int zero_velocity_idx = util::getZeroVelocityIdx( - is_showing_debug_info_, candidate_points, path_points, prev_trajectories_ptr_, *traj_param_); - - auto traj_points = util::convertPointsToTrajectoryPointsWithYaw(candidate_points); - traj_points = util::fillTrajectoryWithVelocity(traj_points, 1e4); - if (prev_trajectories_ptr_) { - const int max_skip_comparison_velocity_idx_for_optimized_points = - calculateNonDecelerationRange(traj_points, *current_ego_pose_ptr_, current_twist_ptr_->twist); - const auto optimized_trajectory = util::concatTraj(*prev_trajectories_ptr_); - traj_points = util::alignVelocityWithPoints( - traj_points, optimized_trajectory, zero_velocity_idx, - max_skip_comparison_velocity_idx_for_optimized_points); - } - const int max_skip_comparison_idx_for_path_points = -1; - traj_points = util::alignVelocityWithPoints( - traj_points, path_points, zero_velocity_idx, max_skip_comparison_idx_for_path_points); - - return traj_points; -} -void ObstacleAvoidancePlanner::publishingDebugData( - const DebugData & debug_data, const autoware_auto_planning_msgs::msg::Path & path, - const std::vector & traj_points, - const VehicleParam & vehicle_param) -{ - auto traj = tier4_autoware_utils::convertToTrajectory(debug_data.foa_data.avoiding_traj_points); - traj.header = path.header; - avoiding_traj_pub_->publish(traj); - - auto debug_smoothed_points = - tier4_autoware_utils::convertToTrajectory(debug_data.smoothed_points); - debug_smoothed_points.header = path.header; - debug_smoothed_points_pub_->publish(debug_smoothed_points); - - tier4_planning_msgs::msg::IsAvoidancePossible is_avoidance_possible; - is_avoidance_possible.is_avoidance_possible = debug_data.foa_data.is_avoidance_possible; - is_avoidance_possible_pub_->publish(is_avoidance_possible); - - std::vector traj_points_debug = traj_points; - // Add z information for virtual wall - if (!traj_points_debug.empty()) { - const int idx = util::getNearestIdx( - path.points, traj_points.back().pose, 0, traj_param_->delta_yaw_threshold_for_closest_point); - traj_points_debug.back().pose.position.z = path.points.at(idx).pose.position.z + 1.0; - } - - debug_markers_pub_->publish( - getDebugVisualizationMarker(debug_data, traj_points_debug, vehicle_param)); - if (is_publishing_area_with_objects_) { - debug_area_with_objects_pub_->publish( - getDebugCostmap(debug_data.area_with_objects_map, path.drivable_area)); - } - if (is_publishing_clearance_map_) { - debug_clearance_map_pub_->publish( - getDebugCostmap(debug_data.clearance_map, path.drivable_area)); - debug_object_clearance_map_pub_->publish( - getDebugCostmap(debug_data.only_object_clearance_map, path.drivable_area)); + // MPT: optimize trajectory to be kinematically feasible and collision free + const auto mpt_trajs = mpt_optimizer_ptr_->getModelPredictiveTrajectory( + enable_avoidance_, eb_traj.get(), path.points, prev_optimal_trajs_ptr_, cv_maps, + current_ego_pose_, current_twist_ptr_->twist.linear.x, debug_data_ptr_); + if (!mpt_trajs) { + return getPrevTrajs(path.points); } + + // make trajectories, which has all optimized trajectories information + Trajectories trajs; + trajs.smoothed_trajectory = eb_traj.get(); + trajs.mpt_ref_points = mpt_trajs.get().ref_points; + trajs.model_predictive_trajectory = mpt_trajs.get().mpt; + + // debug data + debug_data_ptr_->mpt_traj = mpt_trajs.get().mpt; + debug_data_ptr_->mpt_ref_traj = + points_utils::convertToTrajectoryPoints(mpt_trajs.get().ref_points); + debug_data_ptr_->eb_traj = eb_traj.get(); + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return trajs; } -int ObstacleAvoidancePlanner::calculateNonDecelerationRange( - const std::vector & traj_points, - const geometry_msgs::msg::Pose & ego_pose, const geometry_msgs::msg::Twist & ego_twist) const +Trajectories ObstacleAvoidancePlanner::getPrevTrajs( + const std::vector & path_points) const { - const int default_idx = 0; - const int nearest_idx = util::getNearestIdx( - traj_points, ego_pose, default_idx, traj_param_->delta_yaw_threshold_for_closest_point); - const double non_decelerating_arc_length = - (ego_twist.linear.x - traj_param_->max_avoiding_ego_velocity_ms) / - traj_param_->acceleration_for_non_deceleration_range; - if (non_decelerating_arc_length < 0 || traj_points.size() < 2) { - return 0; - } - - double accum_arc_length = 0; - for (std::size_t i = nearest_idx + 1; i < traj_points.size(); i++) { - accum_arc_length += - util::calculate2DDistance(traj_points[i].pose.position, traj_points[i - 1].pose.position); - if (accum_arc_length > non_decelerating_arc_length) { - return i; - } + if (prev_optimal_trajs_ptr_) { + return *prev_optimal_trajs_ptr_; } - return 0; -} -Trajectories ObstacleAvoidancePlanner::getTrajectoryInsideArea( - const Trajectories & trajs, - const std::vector & path_points, - const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, - DebugData * debug_data) const -{ - debug_data->current_vehicle_footprints = - util::getVehicleFootprints(trajs.model_predictive_trajectory, *vehicle_param_); - const auto current_trajs_inside_area = - calcTrajectoryInsideArea(trajs, path_points, road_clearance_map, map_info, debug_data); - if (!current_trajs_inside_area) { - RCLCPP_WARN( - get_logger(), - "[Avoidance] Current trajectory is not inside drivable area, passing previous one. " - "Might stop at the end of drivable trajectory."); - auto prev_trajs = getPrevTrajs(path_points); - const bool is_prev_traj = true; - const auto prev_trajs_inside_area = calcTrajectoryInsideArea( - prev_trajs, path_points, road_clearance_map, map_info, debug_data, is_prev_traj); - return prev_trajs_inside_area.get(); - } - return current_trajs_inside_area.get(); + const auto traj = points_utils::convertToTrajectoryPoints(path_points); + Trajectories trajs; + trajs.smoothed_trajectory = traj; + trajs.model_predictive_trajectory = traj; + return trajs; } -boost::optional ObstacleAvoidancePlanner::calcTrajectoryInsideArea( - const Trajectories & trajs, - const std::vector & path_points, - const cv::Mat & road_clearance_map, const nav_msgs::msg::MapMetaData & map_info, - DebugData * debug_data, const bool is_prev_traj) const +void ObstacleAvoidancePlanner::insertZeroVelocityOutsideDrivableArea( + std::vector & traj_points, + const CVMaps & cv_maps) { - if (!is_stopping_if_outside_drivable_area_) { - const auto stop_idx = getStopIdx(path_points, trajs, map_info, road_clearance_map, debug_data); - if (stop_idx) { - auto clock = rclcpp::Clock(RCL_ROS_TIME); - RCLCPP_WARN_THROTTLE( - get_logger(), clock, std::chrono::milliseconds(1000).count(), - "[Avoidance] Expecting over drivable area"); + stop_watch_.tic(__func__); + + const auto & map_info = cv_maps.map_info; + const auto & road_clearance_map = cv_maps.clearance_map; + + const size_t nearest_idx = + tier4_autoware_utils::findNearestIndex(traj_points, current_ego_pose_.position); + + // NOTE: Some end trajectory points will be ignored to check if outside the drivable area + // since these points might be outside drivable area if only end reference points have high + // curvature. + const size_t end_idx = [&]() { + const size_t enough_long_points_num = + static_cast(traj_param_.num_sampling_points * 0.7); + constexpr size_t end_ignored_points_num = 5; + if (traj_points.size() > enough_long_points_num) { + return std::max(static_cast(0), traj_points.size() - end_ignored_points_num); } - return getBaseTrajectory(path_points, trajs); - } - const auto optional_stop_idx = - getStopIdx(path_points, trajs, map_info, road_clearance_map, debug_data); - if (!is_prev_traj && optional_stop_idx) { - return boost::none; - } - - auto tmp_trajs = getBaseTrajectory(path_points, trajs); - if (is_prev_traj) { - tmp_trajs.extended_trajectory = - std::vector{}; - debug_data->is_expected_to_over_drivable_area = true; - } - - if (optional_stop_idx && !prev_trajectories_ptr_) { - if (optional_stop_idx.get() < static_cast(trajs.model_predictive_trajectory.size())) { - tmp_trajs.model_predictive_trajectory = - std::vector{ - trajs.model_predictive_trajectory.begin(), - trajs.model_predictive_trajectory.begin() + optional_stop_idx.get()}; - tmp_trajs.extended_trajectory = - std::vector{}; - debug_data->is_expected_to_over_drivable_area = true; + return traj_points.size(); + }(); + + for (size_t i = nearest_idx; i < end_idx; ++i) { + const auto & traj_point = traj_points.at(i); + + // calculate the first point being outside drivable area + const bool is_outside = [&]() { + if (use_vehicle_circles_for_drivability_) { + return cv_drivable_area_utils::isOutsideDrivableAreaFromCirclesFootprint( + traj_point, road_clearance_map, map_info, + vehicle_circle_longitudinal_offsets_for_drivability_, + vehicle_circle_radius_for_drivability_); + } + return cv_drivable_area_utils::isOutsideDrivableAreaFromRectangleFootprint( + traj_point, road_clearance_map, map_info, vehicle_param_); + }(); + + // only insert zero velocity to the first point outside drivable area + if (is_outside) { + traj_points[i].longitudinal_velocity_mps = 0.0; + debug_data_ptr_->stop_pose_by_drivable_area = traj_points[i].pose; + break; } } - return tmp_trajs; + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; } -Trajectories ObstacleAvoidancePlanner::getPrevTrajs( - const std::vector & path_points) const +void ObstacleAvoidancePlanner::publishDebugDataInOptimization( + const autoware_auto_planning_msgs::msg::Path & path, + const std::vector & traj_points) { - if (!prev_trajectories_ptr_) { - const auto traj = util::convertPathToTrajectory(path_points); - Trajectories trajs; - trajs.smoothed_trajectory = traj; - trajs.model_predictive_trajectory = traj; - return trajs; - } else { - return *prev_trajectories_ptr_; + stop_watch_.tic(__func__); + + { // publish trajectories + auto debug_eb_traj = tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->eb_traj); + debug_eb_traj.header = path.header; + debug_eb_traj_pub_->publish(debug_eb_traj); + + auto debug_mpt_fixed_traj = + tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->mpt_fixed_traj); + debug_mpt_fixed_traj.header = path.header; + debug_mpt_fixed_traj_pub_->publish(debug_mpt_fixed_traj); + + auto debug_mpt_ref_traj = + tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->mpt_ref_traj); + debug_mpt_ref_traj.header = path.header; + debug_mpt_ref_traj_pub_->publish(debug_mpt_ref_traj); + + auto debug_mpt_traj = tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->mpt_traj); + debug_mpt_traj.header = path.header; + debug_mpt_traj_pub_->publish(debug_mpt_traj); } -} -std::vector -ObstacleAvoidancePlanner::getPrevTrajectory( - const std::vector & path_points) const -{ - std::vector traj; - const auto & trajs = getPrevTrajs(path_points); - traj.insert( - traj.end(), trajs.model_predictive_trajectory.begin(), trajs.model_predictive_trajectory.end()); - traj.insert(traj.end(), trajs.extended_trajectory.begin(), trajs.extended_trajectory.end()); - return traj; + { // publish markers + if (is_publishing_debug_visualization_marker_) { + stop_watch_.tic("getDebugVisualizationMarker"); + const auto & debug_marker = debug_visualization::getDebugVisualizationMarker( + debug_data_ptr_, traj_points, vehicle_param_, false); + debug_data_ptr_->msg_stream << " getDebugVisualizationMarker:= " + << stop_watch_.toc("getDebugVisualizationMarker") << " [ms]\n"; + + stop_watch_.tic("publishDebugVisualizationMarker"); + debug_markers_pub_->publish(debug_marker); + debug_data_ptr_->msg_stream << " publishDebugVisualizationMarker:= " + << stop_watch_.toc("publishDebugVisualizationMarker") + << " [ms]\n"; + } + } + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; } Trajectories ObstacleAvoidancePlanner::makePrevTrajectories( - const geometry_msgs::msg::Pose & ego_pose, const std::vector & path_points, - const Trajectories & trajs) const + const Trajectories & trajs) { + stop_watch_.tic(__func__); + const auto post_processed_smoothed_traj = - generatePostProcessedTrajectory(ego_pose, path_points, trajs.smoothed_trajectory); + generatePostProcessedTrajectory(path_points, trajs.smoothed_trajectory); + + // TODO(murooka) generatePoseProcessedTrajectory may be too large Trajectories trajectories; trajectories.smoothed_trajectory = post_processed_smoothed_traj; trajectories.mpt_ref_points = trajs.mpt_ref_points; trajectories.model_predictive_trajectory = trajs.model_predictive_trajectory; - trajectories.extended_trajectory = trajs.extended_trajectory; + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return trajectories; } -Trajectories ObstacleAvoidancePlanner::getBaseTrajectory( +std::vector +ObstacleAvoidancePlanner::generatePostProcessedTrajectory( const std::vector & path_points, - const Trajectories & trajs) const + const std::vector & optimized_traj_points) { - auto base_trajs = trajs; - if (!trajs.extended_trajectory.empty()) { - const auto extended_point_opt = util::getLastExtendedTrajPoint( - path_points.back(), trajs.extended_trajectory.back().pose, - traj_param_->delta_yaw_threshold_for_closest_point, - traj_param_->max_dist_for_extending_end_point); - if (extended_point_opt) { - base_trajs.extended_trajectory.push_back(extended_point_opt.get()); - } - } else if (!trajs.model_predictive_trajectory.empty()) { - const auto extended_point_opt = util::getLastExtendedTrajPoint( - path_points.back(), trajs.model_predictive_trajectory.back().pose, - traj_param_->delta_yaw_threshold_for_closest_point, - traj_param_->max_dist_for_extending_end_point); - if (extended_point_opt) { - base_trajs.extended_trajectory.push_back(extended_point_opt.get()); - } + stop_watch_.tic(__func__); + + std::vector trajectory_points; + if (path_points.empty()) { + autoware_auto_planning_msgs::msg::TrajectoryPoint tmp_point; + tmp_point.pose = current_ego_pose_; + tmp_point.longitudinal_velocity_mps = 0.0; + trajectory_points.push_back(tmp_point); + return trajectory_points; } - double prev_velocity = 1e4; - for (auto & p : base_trajs.model_predictive_trajectory) { - if (p.longitudinal_velocity_mps < 1e-6) { - p.longitudinal_velocity_mps = prev_velocity; - } else { - prev_velocity = p.longitudinal_velocity_mps; - } + if (optimized_traj_points.empty()) { + trajectory_points = points_utils::convertToTrajectoryPoints(path_points); + return trajectory_points; } - for (auto & p : base_trajs.extended_trajectory) { - if (p.longitudinal_velocity_mps < 1e-6) { - p.longitudinal_velocity_mps = prev_velocity; - } else { - prev_velocity = p.longitudinal_velocity_mps; + + // calculate extended trajectory that connects to optimized trajectory smoothly + const auto extended_traj_points = getExtendedTrajectory(path_points, optimized_traj_points); + + // concat trajectories + const auto full_traj_points = + points_utils::concatTrajectory(optimized_traj_points, extended_traj_points); + + // NOTE: fine_traj_points has no velocity information + const auto fine_traj_points = generateFineTrajectoryPoints(path_points, full_traj_points); + + const auto fine_traj_points_with_vel = + alignVelocity(fine_traj_points, path_points, full_traj_points); + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return fine_traj_points_with_vel; +} + +std::vector +ObstacleAvoidancePlanner::getExtendedTrajectory( + const std::vector & path_points, + const std::vector & optimized_points) +{ + stop_watch_.tic(__func__); + + assert(!path_points.empty()); + + const double accum_arc_length = tier4_autoware_utils::calcArcLength(optimized_points); + if (accum_arc_length > traj_param_.trajectory_length) { + RCLCPP_INFO_THROTTLE( + this->get_logger(), *this->get_clock(), std::chrono::milliseconds(10000).count(), + "[Avoidance] Not extend trajectory"); + return std::vector{}; + } + + // calculate end idx of optimized points on path points + const auto opt_end_path_idx = tier4_autoware_utils::findNearestIndex( + path_points, optimized_points.back().pose, std::numeric_limits::max(), + traj_param_.delta_yaw_threshold_for_closest_point); + if (!opt_end_path_idx) { + RCLCPP_INFO_THROTTLE( + this->get_logger(), *this->get_clock(), std::chrono::milliseconds(10000).count(), + "[Avoidance] Not extend trajectory since could not find nearest idx from last opt point"); + return std::vector{}; + } + + auto extended_traj_points = + [&]() -> std::vector { + constexpr double non_fixed_traj_length = 5.0; // TODO(murooka) may be better to tune + const size_t non_fixed_begin_path_idx = opt_end_path_idx.get(); + const size_t non_fixed_end_path_idx = + points_utils::findForwardIndex(path_points, non_fixed_begin_path_idx, non_fixed_traj_length); + + if ( + non_fixed_begin_path_idx == path_points.size() - 1 || + non_fixed_begin_path_idx == non_fixed_end_path_idx) { + if (points_utils::isNearLastPathPoint( + optimized_points.back(), path_points, traj_param_.max_dist_for_extending_end_point, + traj_param_.delta_yaw_threshold_for_closest_point)) { + return std::vector{}; + } + const auto last_traj_point = points_utils::convertToTrajectoryPoint(path_points.back()); + return std::vector{last_traj_point}; + } else if (non_fixed_end_path_idx == path_points.size() - 1) { + // no need to connect smoothly since extended trajectory length is short enough + const auto last_traj_point = points_utils::convertToTrajectoryPoint(path_points.back()); + return std::vector{last_traj_point}; } + + // define non_fixed/fixed_traj_points + const auto begin_point = optimized_points.back(); + const auto end_point = + points_utils::convertToTrajectoryPoint(path_points.at(non_fixed_end_path_idx)); + const std::vector non_fixed_traj_points{ + begin_point, end_point}; + const std::vector fixed_path_points{ + path_points.begin() + non_fixed_end_path_idx + 1, path_points.end()}; + const auto fixed_traj_points = points_utils::convertToTrajectoryPoints(fixed_path_points); + + // spline interpolation to two traj points with end diff constraints + const double begin_yaw = tf2::getYaw(begin_point.pose.orientation); + const double end_yaw = tf2::getYaw(end_point.pose.orientation); + const auto interpolated_non_fixed_traj_points = + interpolation_utils::getConnectedInterpolatedPoints( + non_fixed_traj_points, eb_param_.delta_arc_length_for_eb, begin_yaw, end_yaw); + + // concat interpolated_non_fixed and fixed traj points + auto extended_points = interpolated_non_fixed_traj_points; + extended_points.insert( + extended_points.end(), fixed_traj_points.begin(), fixed_traj_points.end()); + + debug_data_ptr_->extended_non_fixed_traj = interpolated_non_fixed_traj_points; + debug_data_ptr_->extended_fixed_traj = fixed_traj_points; + + return extended_points; + }(); + + // NOTE: Extended points will be concatenated with optimized points. + // Then, minimum velocity of each point is chosen from concatenated points or path points + // since optimized points has zero velocity outside drivable area + constexpr double large_velocity = 1e4; + for (auto & point : extended_traj_points) { + point.longitudinal_velocity_mps = large_velocity; } - return base_trajs; + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return extended_traj_points; +} + +std::vector +ObstacleAvoidancePlanner::generateFineTrajectoryPoints( + const std::vector & path_points, + const std::vector & traj_points) const +{ + stop_watch_.tic(__func__); + + // interpolate x and y + auto interpolated_traj_points = interpolation_utils::getInterpolatedTrajectoryPoints( + traj_points, traj_param_.delta_arc_length_for_trajectory); + + // calculate yaw from x and y + // NOTE: We do not use spline interpolation to yaw in behavior path since the yaw is unstable. + // Currently this implementation is removed since this calculation is heavy (~20ms) + // fillYawInTrajectoryPoint(interpolated_traj_points); + + // compensate last pose + points_utils::compensateLastPose( + path_points.back(), interpolated_traj_points, traj_param_.max_dist_for_extending_end_point, + traj_param_.delta_yaw_threshold_for_closest_point); + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + + return interpolated_traj_points; } -boost::optional ObstacleAvoidancePlanner::getStopIdx( +std::vector +ObstacleAvoidancePlanner::alignVelocity( + const std::vector & fine_traj_points, const std::vector & path_points, - const Trajectories & trajs, const nav_msgs::msg::MapMetaData & map_info, - const cv::Mat & road_clearance_map, DebugData * debug_data) const + const std::vector & traj_points) const { - const int nearest_idx = util::getNearestIdx( - path_points, *current_ego_pose_ptr_, 0, traj_param_->delta_yaw_threshold_for_closest_point); - const double accum_ds = util::getArcLength(path_points, nearest_idx); - - auto target_points = trajs.model_predictive_trajectory; - if (accum_ds < traj_param_->num_sampling_points * traj_param_->delta_arc_length_for_mpt_points) { - target_points.insert( - target_points.end(), trajs.extended_trajectory.begin(), trajs.extended_trajectory.end()); - const auto extended_mpt_point_opt = util::getLastExtendedTrajPoint( - path_points.back(), trajs.model_predictive_trajectory.back().pose, - traj_param_->delta_yaw_threshold_for_closest_point, - traj_param_->max_dist_for_extending_end_point); - if (extended_mpt_point_opt) { - target_points.push_back(extended_mpt_point_opt.get()); + stop_watch_.tic(__func__); + + // search zero velocity index of fine_traj_points + const size_t zero_vel_fine_traj_idx = [&]() { + const size_t zero_vel_path_idx = searchExtendedZeroVelocityIndex(fine_traj_points, path_points); + const size_t zero_vel_traj_idx = + searchExtendedZeroVelocityIndex(fine_traj_points, traj_points); // for outside drivable area + + return std::min(zero_vel_path_idx, zero_vel_traj_idx); + }(); + + auto fine_traj_points_with_vel = fine_traj_points; + size_t prev_begin_idx = 0; + for (size_t i = 0; i < fine_traj_points_with_vel.size(); ++i) { + const auto truncated_points = points_utils::clipForwardPoints(path_points, prev_begin_idx, 5.0); + + const auto & target_pos = fine_traj_points_with_vel[i].pose.position; + const size_t closest_seg_idx = + tier4_autoware_utils::findNearestSegmentIndex(truncated_points, target_pos); + + // lerp z + fine_traj_points_with_vel[i].pose.position.z = + lerpPoseZ(truncated_points, target_pos, closest_seg_idx); + + // lerp vx + const double target_vel = lerpTwistX(truncated_points, target_pos, closest_seg_idx); + if (i >= zero_vel_fine_traj_idx) { + fine_traj_points_with_vel[i].longitudinal_velocity_mps = 0.0; + } else if (target_vel < 1e-6) { // NOTE: velocity may be negative due to linear interpolation + const auto prev_idx = std::max(static_cast(i) - 1, 0); + fine_traj_points_with_vel[i].longitudinal_velocity_mps = + fine_traj_points_with_vel[prev_idx].longitudinal_velocity_mps; + } else { + fine_traj_points_with_vel[i].longitudinal_velocity_mps = target_vel; } + + // NOTE: closest_seg_idx is for the clipped trajectory. This operation must be "+=". + prev_begin_idx += closest_seg_idx; + } + + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; + return fine_traj_points_with_vel; +} + +void ObstacleAvoidancePlanner::publishDebugDataInMain( + const autoware_auto_planning_msgs::msg::Path & path) const +{ + stop_watch_.tic(__func__); + + { // publish trajectories + auto debug_extended_fixed_traj = + tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->extended_fixed_traj); + debug_extended_fixed_traj.header = path.header; + debug_extended_fixed_traj_pub_->publish(debug_extended_fixed_traj); + + auto debug_extended_non_fixed_traj = + tier4_autoware_utils::convertToTrajectory(debug_data_ptr_->extended_non_fixed_traj); + debug_extended_non_fixed_traj.header = path.header; + debug_extended_non_fixed_traj_pub_->publish(debug_extended_non_fixed_traj); } - const auto footprints = util::getVehicleFootprints(target_points, *vehicle_param_); - const int debug_nearest_footprint_idx = - util::getNearestIdx(target_points, current_ego_pose_ptr_->position); - std::vector debug_truncated_footprints( - footprints.begin() + debug_nearest_footprint_idx, footprints.end()); - debug_data->vehicle_footprints = debug_truncated_footprints; + { // publish clearance map + stop_watch_.tic("publishClearanceMap"); + + if (is_publishing_area_with_objects_) { // false + debug_area_with_objects_pub_->publish(debug_visualization::getDebugCostmap( + debug_data_ptr_->area_with_objects_map, path.drivable_area)); + } + if (is_publishing_object_clearance_map_) { // false + debug_object_clearance_map_pub_->publish(debug_visualization::getDebugCostmap( + debug_data_ptr_->only_object_clearance_map, path.drivable_area)); + } + if (is_publishing_clearance_map_) { // true + debug_clearance_map_pub_->publish( + debug_visualization::getDebugCostmap(debug_data_ptr_->clearance_map, path.drivable_area)); + } + debug_data_ptr_->msg_stream << " getDebugCostMap * 3:= " + << stop_watch_.toc("publishClearanceMap") << " [ms]\n"; + } - const auto optional_idx = - process_cv::getStopIdx(footprints, *current_ego_pose_ptr_, road_clearance_map, map_info); - return optional_idx; + debug_data_ptr_->msg_stream << " " << __func__ << ":= " << stop_watch_.toc(__func__) + << " [ms]\n"; } -#include +#include "rclcpp_components/register_node_macro.hpp" RCLCPP_COMPONENTS_REGISTER_NODE(ObstacleAvoidancePlanner) diff --git a/planning/obstacle_avoidance_planner/src/process_cv.cpp b/planning/obstacle_avoidance_planner/src/process_cv.cpp index 45de56e21804f..f35a9ada665f0 100644 --- a/planning/obstacle_avoidance_planner/src/process_cv.cpp +++ b/planning/obstacle_avoidance_planner/src/process_cv.cpp @@ -14,7 +14,6 @@ #include "obstacle_avoidance_planner/process_cv.hpp" #include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/util.hpp" #include #include diff --git a/planning/obstacle_avoidance_planner/src/util.cpp b/planning/obstacle_avoidance_planner/src/util.cpp deleted file mode 100644 index 527726e5cd801..0000000000000 --- a/planning/obstacle_avoidance_planner/src/util.cpp +++ /dev/null @@ -1,1018 +0,0 @@ -// Copyright 2020 Tier IV, Inc. -// -// 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 "obstacle_avoidance_planner/util.hpp" - -#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/mpt_optimizer.hpp" - -#include - -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include - -namespace util -{ -template -geometry_msgs::msg::Point transformToRelativeCoordinate2D( - const T & point, const geometry_msgs::msg::Pose & origin) -{ - // NOTE: implement transformation without defining yaw variable - // but directly sin/cos of yaw for fast calculation - const auto & q = origin.orientation; - const double cos_yaw = 1 - 2 * q.z * q.z; - const double sin_yaw = 2 * q.w * q.z; - - geometry_msgs::msg::Point relative_p; - const double tmp_x = point.x - origin.position.x; - const double tmp_y = point.y - origin.position.y; - relative_p.x = tmp_x * cos_yaw + tmp_y * sin_yaw; - relative_p.y = -tmp_x * sin_yaw + tmp_y * cos_yaw; - relative_p.z = point.z; - - return relative_p; -} - -template geometry_msgs::msg::Point transformToRelativeCoordinate2D( - const geometry_msgs::msg::Point &, const geometry_msgs::msg::Pose & origin); -template geometry_msgs::msg::Point transformToRelativeCoordinate2D( - const geometry_msgs::msg::Point32 &, const geometry_msgs::msg::Pose & origin); - -geometry_msgs::msg::Point transformToAbsoluteCoordinate2D( - const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Pose & origin) -{ - // NOTE: implement transformation without defining yaw variable - // but directly sin/cos of yaw for fast calculation - const auto & q = origin.orientation; - const double cos_yaw = 1 - 2 * q.z * q.z; - const double sin_yaw = 2 * q.w * q.z; - - geometry_msgs::msg::Point absolute_p; - absolute_p.x = point.x * cos_yaw - point.y * sin_yaw + origin.position.x; - absolute_p.y = point.x * sin_yaw + point.y * cos_yaw + origin.position.y; - absolute_p.z = point.z; - - return absolute_p; -} - -double calculate2DDistance(const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b) -{ - const double dx = a.x - b.x; - const double dy = a.y - b.y; - return std::sqrt(dx * dx + dy * dy); -} - -double calculateSquaredDistance( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b) -{ - const double dx = a.x - b.x; - const double dy = a.y - b.y; - return dx * dx + dy * dy; -} - -double getYawFromPoints( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root) -{ - const double dx = a.x - a_root.x; - const double dy = a.y - a_root.y; - return std::atan2(dy, dx); -} - -geometry_msgs::msg::Quaternion getQuaternionFromYaw(const double yaw) -{ - tf2::Quaternion q; - q.setRPY(0, 0, yaw); - return tf2::toMsg(q); -} - -double normalizeRadian(const double angle) -{ - double n_angle = std::fmod(angle, 2 * M_PI); - n_angle = n_angle > M_PI ? n_angle - 2 * M_PI : n_angle < -M_PI ? 2 * M_PI + n_angle : n_angle; - return n_angle; -} - -geometry_msgs::msg::Quaternion getQuaternionFromPoints( - const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root) -{ - const double roll = 0; - const double pitch = 0; - const double yaw = util::getYawFromPoints(a, a_root); - tf2::Quaternion quaternion; - quaternion.setRPY(roll, pitch, yaw); - return tf2::toMsg(quaternion); -} - -template -geometry_msgs::msg::Point transformMapToImage( - const T & map_point, const nav_msgs::msg::MapMetaData & occupancy_grid_info) -{ - geometry_msgs::msg::Point relative_p = - transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); - double resolution = occupancy_grid_info.resolution; - double map_y_height = occupancy_grid_info.height; - double map_x_width = occupancy_grid_info.width; - double map_x_in_image_resolution = relative_p.x / resolution; - double map_y_in_image_resolution = relative_p.y / resolution; - geometry_msgs::msg::Point image_point; - image_point.x = map_y_height - map_y_in_image_resolution; - image_point.y = map_x_width - map_x_in_image_resolution; - return image_point; -} -template geometry_msgs::msg::Point transformMapToImage( - const geometry_msgs::msg::Point &, const nav_msgs::msg::MapMetaData & map_info); -template geometry_msgs::msg::Point transformMapToImage( - const geometry_msgs::msg::Point32 &, const nav_msgs::msg::MapMetaData & map_info); - -boost::optional transformMapToOptionalImage( - const geometry_msgs::msg::Point & map_point, - const nav_msgs::msg::MapMetaData & occupancy_grid_info) -{ - geometry_msgs::msg::Point relative_p = - transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); - double resolution = occupancy_grid_info.resolution; - double map_y_height = occupancy_grid_info.height; - double map_x_width = occupancy_grid_info.width; - double map_x_in_image_resolution = relative_p.x / resolution; - double map_y_in_image_resolution = relative_p.y / resolution; - double image_x = map_y_height - map_y_in_image_resolution; - double image_y = map_x_width - map_x_in_image_resolution; - if ( - image_x >= 0 && image_x < static_cast(map_y_height) && image_y >= 0 && - image_y < static_cast(map_x_width)) { - geometry_msgs::msg::Point image_point; - image_point.x = image_x; - image_point.y = image_y; - return image_point; - } else { - return boost::none; - } -} - -bool transformMapToImage( - const geometry_msgs::msg::Point & map_point, - const nav_msgs::msg::MapMetaData & occupancy_grid_info, geometry_msgs::msg::Point & image_point) -{ - geometry_msgs::msg::Point relative_p = - transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); - const double map_y_height = occupancy_grid_info.height; - const double map_x_width = occupancy_grid_info.width; - const double scale = 1 / occupancy_grid_info.resolution; - const double map_x_in_image_resolution = relative_p.x * scale; - const double map_y_in_image_resolution = relative_p.y * scale; - const double image_x = map_y_height - map_y_in_image_resolution; - const double image_y = map_x_width - map_x_in_image_resolution; - if ( - image_x >= 0 && image_x < static_cast(map_y_height) && image_y >= 0 && - image_y < static_cast(map_x_width)) { - image_point.x = image_x; - image_point.y = image_y; - return true; - } else { - return false; - } -} - -bool interpolate2DPoints( - const std::vector & base_x, const std::vector & base_y, const double resolution, - std::vector & interpolated_points) -{ - if (base_x.empty() || base_y.empty()) { - return false; - } - std::vector base_s = calcEuclidDist(base_x, base_y); - if (base_s.empty() || base_s.size() == 1) { - return false; - } - std::vector new_s; - for (double i = 0.0; i < base_s.back() - 1e-6; i += resolution) { - new_s.push_back(i); - } - - // spline interpolation - const std::vector interpolated_x = interpolation::slerp(base_s, base_x, new_s); - const std::vector interpolated_y = interpolation::slerp(base_s, base_y, new_s); - - for (size_t i = 0; i < interpolated_x.size(); i++) { - if (std::isnan(interpolated_x[i]) || std::isnan(interpolated_y[i])) { - return false; - } - } - for (size_t i = 0; i < interpolated_x.size(); i++) { - geometry_msgs::msg::Point point; - point.x = interpolated_x[i]; - point.y = interpolated_y[i]; - interpolated_points.push_back(point); - } - return true; -} - -std::vector getInterpolatedPoints( - const std::vector & first_points, - const std::vector & second_points, const double delta_arc_length) -{ - std::vector tmp_x; - std::vector tmp_y; - std::vector concat_points; - for (const auto point : first_points) { - concat_points.push_back(point.position); - } - for (const auto & point : second_points) { - concat_points.push_back(point.position); - } - - for (std::size_t i = 0; i < concat_points.size(); i++) { - if (i > 0) { - if ( - std::fabs(concat_points[i].x - concat_points[i - 1].x) < 1e-6 && - std::fabs(concat_points[i].y - concat_points[i - 1].y) < 1e-6) { - continue; - } - } - tmp_x.push_back(concat_points[i].x); - tmp_y.push_back(concat_points[i].y); - } - std::vector interpolated_points; - util::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, interpolated_points); - return interpolated_points; -} - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length) -{ - std::vector tmp_x; - std::vector tmp_y; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - if ( - std::fabs(points[i].x - points[i - 1].x) < 1e-6 && - std::fabs(points[i].y - points[i - 1].y) < 1e-6) { - continue; - } - } - tmp_x.push_back(points[i].x); - tmp_y.push_back(points[i].y); - } - std::vector interpolated_points; - util::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, interpolated_points); - return interpolated_points; -} - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length) -{ - std::vector tmp_x; - std::vector tmp_y; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - if ( - std::fabs(points[i].position.x - points[i - 1].position.x) < 1e-6 && - std::fabs(points[i].position.y - points[i - 1].position.y) < 1e-6) { - continue; - } - } - tmp_x.push_back(points[i].position.x); - tmp_y.push_back(points[i].position.y); - } - std::vector interpolated_points; - util::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, interpolated_points); - return interpolated_points; -} - -std::vector getInterpolatedPoints( - const std::vector & points, const double delta_arc_length) -{ - std::vector tmp_x; - std::vector tmp_y; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - if ( - std::fabs(points[i].p.x - points[i - 1].p.x) < 1e-6 && - std::fabs(points[i].p.y - points[i - 1].p.y) < 1e-6) { - continue; - } - } - tmp_x.push_back(points[i].p.x); - tmp_y.push_back(points[i].p.y); - } - std::vector interpolated_points; - util::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, interpolated_points); - return interpolated_points; -} - -template -std::vector getInterpolatedPoints( - const T & points, const double delta_arc_length) -{ - std::vector tmp_x; - std::vector tmp_y; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - if ( - std::fabs(points[i].pose.position.x - points[i - 1].pose.position.x) < 1e-6 && - std::fabs(points[i].pose.position.y - points[i - 1].pose.position.y) < 1e-6) { - continue; - } - } - tmp_x.push_back(points[i].pose.position.x); - tmp_y.push_back(points[i].pose.position.y); - } - std::vector interpolated_points; - util::interpolate2DPoints(tmp_x, tmp_y, delta_arc_length, interpolated_points); - return interpolated_points; -} -template std::vector -getInterpolatedPoints>( - const std::vector &, const double); -template std::vector -getInterpolatedPoints>( - const std::vector &, const double); - -template -int getNearestIdx( - const T & points, const geometry_msgs::msg::Pose & pose, const int default_idx, - const double delta_yaw_threshold) -{ - double min_dist = std::numeric_limits::max(); - int nearest_idx = default_idx; - const double point_yaw = tf2::getYaw(pose.orientation); - for (std::size_t i = 0; i < points.size(); i++) { - const double dist = calculateSquaredDistance(points[i].pose.position, pose.position); - const double points_yaw = tf2::getYaw(points[i].pose.orientation); - const double diff_yaw = points_yaw - point_yaw; - const double norm_diff_yaw = normalizeRadian(diff_yaw); - if (dist < min_dist && std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - min_dist = dist; - nearest_idx = i; - } - } - return nearest_idx; -} -template int getNearestIdx>( - const std::vector &, - const geometry_msgs::msg::Pose &, const int, const double); -template int getNearestIdx>( - const std::vector &, - const geometry_msgs::msg::Pose &, const int, const double); - -int getNearestIdx( - const std::vector & points, const geometry_msgs::msg::Pose & pose, - const int default_idx, const double delta_yaw_threshold, const double delta_dist_threshold) -{ - int nearest_idx = default_idx; - double min_dist = std::numeric_limits::max(); - const double point_yaw = tf2::getYaw(pose.orientation); - for (std::size_t i = 0; i < points.size(); i++) { - const double dist = calculateSquaredDistance(points[i], pose.position); - double points_yaw = 0; - if (i > 0) { - const double dx = points[i].x - points[i - 1].x; - const double dy = points[i].y - points[i - 1].y; - points_yaw = std::atan2(dy, dx); - } else if (i == 0 && points.size() > 1) { - const double dx = points[i + 1].x - points[i].x; - const double dy = points[i + 1].y - points[i].y; - points_yaw = std::atan2(dy, dx); - } - const double diff_yaw = points_yaw - point_yaw; - const double norm_diff_yaw = normalizeRadian(diff_yaw); - if ( - dist < min_dist && dist < delta_dist_threshold && - std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - min_dist = dist; - nearest_idx = i; - } - } - return nearest_idx; -} - -int getNearestIdxOverPoint( - const std::vector & points, - const geometry_msgs::msg::Pose & pose, [[maybe_unused]] const int default_idx, - const double delta_yaw_threshold) -{ - double min_dist = std::numeric_limits::max(); - const double point_yaw = tf2::getYaw(pose.orientation); - const double pose_dx = std::cos(point_yaw); - const double pose_dy = std::sin(point_yaw); - int nearest_idx = 0; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - const double dist = util::calculateSquaredDistance(points[i].pose.position, pose.position); - const double points_yaw = - getYawFromPoints(points[i].pose.position, points[i - 1].pose.position); - const double diff_yaw = points_yaw - point_yaw; - const double norm_diff_yaw = normalizeRadian(diff_yaw); - const double dx = points[i].pose.position.x - pose.position.x; - const double dy = points[i].pose.position.y - pose.position.y; - const double ip = dx * pose_dx + dy * pose_dy; - if (dist < min_dist && ip > 0 && std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - min_dist = dist; - nearest_idx = i; - } - } - } - return nearest_idx; -} - -int getNearestIdx( - const std::vector & points, const geometry_msgs::msg::Pose & pose, - const int default_idx, const double delta_yaw_threshold) -{ - double min_dist = std::numeric_limits::max(); - int nearest_idx = default_idx; - const double point_yaw = tf2::getYaw(pose.orientation); - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - const double dist = calculateSquaredDistance(points[i], pose.position); - const double points_yaw = getYawFromPoints(points[i], points[i - 1]); - const double diff_yaw = points_yaw - point_yaw; - const double norm_diff_yaw = normalizeRadian(diff_yaw); - if (dist < min_dist && std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - min_dist = dist; - nearest_idx = i; - } - } - } - return nearest_idx; -} - -template -int getNearestIdx(const T & points, const geometry_msgs::msg::Point & point, const int default_idx) -{ - double min_dist = std::numeric_limits::max(); - int nearest_idx = default_idx; - for (std::size_t i = 0; i < points.size(); i++) { - if (i > 0) { - const double dist = calculateSquaredDistance(points[i - 1].pose.position, point); - const double points_dx = points[i].pose.position.x - points[i - 1].pose.position.x; - const double points_dy = points[i].pose.position.y - points[i - 1].pose.position.y; - const double points2pose_dx = point.x - points[i - 1].pose.position.x; - const double points2pose_dy = point.y - points[i - 1].pose.position.y; - const double ip = points_dx * points2pose_dx + points_dy * points2pose_dy; - if (ip < 0) { - return nearest_idx; - } - if (dist < min_dist && ip > 0) { - min_dist = dist; - nearest_idx = i - 1; - } - } - } - return nearest_idx; -} -template int getNearestIdx>( - const std::vector & points, - const geometry_msgs::msg::Point & point, const int default_idx); -template int getNearestIdx>( - const std::vector & points, - const geometry_msgs::msg::Point & point, const int default_idx); - -int getNearestIdx( - const std::vector & points, const geometry_msgs::msg::Point & point) -{ - int nearest_idx = 0; - double min_dist = std::numeric_limits::max(); - for (std::size_t i = 0; i < points.size(); i++) { - const double dist = calculateSquaredDistance(points[i], point); - if (dist < min_dist) { - min_dist = dist; - nearest_idx = i; - } - } - return nearest_idx; -} - -template -int getNearestIdx(const T & points, const geometry_msgs::msg::Point & point) -{ - int nearest_idx = 0; - double min_dist = std::numeric_limits::max(); - for (std::size_t i = 0; i < points.size(); i++) { - const double dist = calculateSquaredDistance(points[i].pose.position, point); - if (dist < min_dist) { - min_dist = dist; - nearest_idx = i; - } - } - return nearest_idx; -} -template int getNearestIdx>( - const std::vector &, - const geometry_msgs::msg::Point &); -template int getNearestIdx>( - const std::vector &, - const geometry_msgs::msg::Point &); - -int getNearestIdx( - const std::vector & points, const double target_s, const int begin_idx) -{ - double nearest_delta_s = std::numeric_limits::max(); - int nearest_idx = begin_idx; - for (std::size_t i = begin_idx; i < points.size(); i++) { - double diff = std::fabs(target_s - points[i].s); - if (diff < nearest_delta_s) { - nearest_delta_s = diff; - nearest_idx = i; - } - } - return nearest_idx; -} - -template -int getNearestPointIdx(const T & points, const geometry_msgs::msg::Point & point) -{ - int nearest_idx = 0; - double min_dist = std::numeric_limits::max(); - for (std::size_t i = 0; i < points.size(); i++) { - const double dist = calculateSquaredDistance(points[i].p, point); - if (dist < min_dist) { - min_dist = dist; - nearest_idx = i; - } - } - return nearest_idx; -} -template int getNearestPointIdx>( - const std::vector &, const geometry_msgs::msg::Point &); -template int getNearestPointIdx>( - const std::vector &, const geometry_msgs::msg::Point &); - -std::vector convertPathToTrajectory( - const std::vector & path_points) -{ - std::vector traj_points; - for (const auto & point : path_points) { - autoware_auto_planning_msgs::msg::TrajectoryPoint tmp_point; - tmp_point.pose = point.pose; - tmp_point.longitudinal_velocity_mps = point.longitudinal_velocity_mps; - traj_points.push_back(tmp_point); - } - return traj_points; -} - -std::vector -convertPointsToTrajectoryPointsWithYaw(const std::vector & points) -{ - std::vector traj_points; - for (size_t i = 0; i < points.size(); i++) { - autoware_auto_planning_msgs::msg::TrajectoryPoint traj_point; - traj_point.pose.position = points[i]; - double yaw = 0; - if (i > 0) { - const double dx = points[i].x - points[i - 1].x; - const double dy = points[i].y - points[i - 1].y; - yaw = std::atan2(dy, dx); - } else if (i == 0 && points.size() > 1) { - const double dx = points[i + 1].x - points[i].x; - const double dy = points[i + 1].y - points[i].y; - yaw = std::atan2(dy, dx); - } - const double roll = 0; - const double pitch = 0; - tf2::Quaternion quaternion; - quaternion.setRPY(roll, pitch, yaw); - traj_point.pose.orientation = tf2::toMsg(quaternion); - traj_points.push_back(traj_point); - } - return traj_points; -} - -std::vector fillTrajectoryWithVelocity( - const std::vector & traj_points, - const double velocity) -{ - std::vector traj_with_velocity; - for (const auto & traj_point : traj_points) { - auto tmp_point = traj_point; - tmp_point.longitudinal_velocity_mps = velocity; - traj_with_velocity.push_back(tmp_point); - } - return traj_with_velocity; -} - -template -std::vector alignVelocityWithPoints( - const std::vector & base_traj_points, - const T & points, const int zero_velocity_traj_idx, const int max_skip_comparison_idx) -{ - auto traj_points = base_traj_points; - int prev_begin_idx = 0; - for (std::size_t i = 0; i < traj_points.size(); i++) { - const auto first = points.begin() + prev_begin_idx; - - // TODO(Horibe) could be replaced end() with some reasonable number to reduce computational time - const auto last = points.end(); - const T truncated_points(first, last); - - const size_t closest_seg_idx = - tier4_autoware_utils::findNearestSegmentIndex(truncated_points, traj_points[i].pose.position); - // TODO(murooka) implement calcSignedArcLength(points, idx, point) - const double closest_to_target_dist = tier4_autoware_utils::calcSignedArcLength( - truncated_points, truncated_points.at(closest_seg_idx).pose.position, - traj_points[i].pose.position); - const double seg_dist = tier4_autoware_utils::calcSignedArcLength( - truncated_points, closest_seg_idx, closest_seg_idx + 1); - - // interpolate 1st-nearest (v1) value and 2nd-nearest value (v2) - const auto lerp = [&](const double v1, const double v2, const double ratio) { - return std::abs(seg_dist) < 1e-6 ? v2 : v1 + (v2 - v1) * ratio; - }; - - // z - { - const double closest_z = truncated_points.at(closest_seg_idx).pose.position.z; - const double next_z = truncated_points.at(closest_seg_idx + 1).pose.position.z; - traj_points[i].pose.position.z = lerp(closest_z, next_z, closest_to_target_dist / seg_dist); - } - - // vx - { - const double closest_vel = truncated_points[closest_seg_idx].longitudinal_velocity_mps; - const double next_vel = truncated_points[closest_seg_idx + 1].longitudinal_velocity_mps; - const double target_vel = lerp(closest_vel, next_vel, closest_to_target_dist / seg_dist); - - if (static_cast(i) >= zero_velocity_traj_idx) { - traj_points[i].longitudinal_velocity_mps = 0; - } else if (target_vel < 1e-6) { - const auto idx = std::max(static_cast(i) - 1, 0); - traj_points[i].longitudinal_velocity_mps = traj_points[idx].longitudinal_velocity_mps; - } else if (static_cast(i) <= max_skip_comparison_idx) { - traj_points[i].longitudinal_velocity_mps = target_vel; - } else { - traj_points[i].longitudinal_velocity_mps = - std::fmin(target_vel, traj_points[i].longitudinal_velocity_mps); - } - } - // NOTE: closest_seg_idx is for the clipped trajectory. This operation must be "+=". - prev_begin_idx += closest_seg_idx; - } - return traj_points; -} -template std::vector -alignVelocityWithPoints>( - const std::vector &, - const std::vector &, const int, const int); -template std::vector -alignVelocityWithPoints>( - const std::vector &, - const std::vector &, const int, const int); - -std::vector> getHistogramTable(const std::vector> & input) -{ - std::vector> histogram_table = input; - for (std::size_t i = 0; i < input.size(); i++) { - for (std::size_t j = 0; j < input[i].size(); j++) { - if (input[i][j]) { - histogram_table[i][j] = 0; - } else { - histogram_table[i][j] = (i > 0) ? histogram_table[i - 1][j] + 1 : 1; - } - } - } - return histogram_table; -} - -struct HistogramBin -{ - int height; - int variable_pos; - int original_pos; -}; - -Rectangle getLargestRectangleInRow( - const std::vector & histo, const int current_row, [[maybe_unused]] const int row_size) -{ - std::vector search_histo = histo; - search_histo.push_back(0); - std::stack stack; - Rectangle largest_rect; - for (std::size_t i = 0; i < search_histo.size(); i++) { - HistogramBin bin; - bin.height = search_histo[i]; - bin.variable_pos = i; - bin.original_pos = i; - if (stack.empty()) { - stack.push(bin); - } else { - if (stack.top().height < bin.height) { - stack.push(bin); - } else if (stack.top().height >= bin.height) { - int target_i = i; - while (!stack.empty() && bin.height <= stack.top().height) { - HistogramBin tmp_bin = stack.top(); - stack.pop(); - int area = (i - tmp_bin.variable_pos) * tmp_bin.height; - if (area > largest_rect.area) { - largest_rect.max_y_idx = tmp_bin.variable_pos; - largest_rect.min_y_idx = i - 1; - largest_rect.max_x_idx = current_row - tmp_bin.height + 1; - largest_rect.min_x_idx = current_row; - largest_rect.area = area; - } - - target_i = tmp_bin.variable_pos; - } - bin.variable_pos = target_i; - stack.push(bin); - } - } - } - return largest_rect; -} - -Rectangle getLargestRectangle(const std::vector> & input) -{ - std::vector> histogram_table = getHistogramTable(input); - Rectangle largest_rectangle; - for (std::size_t i = 0; i < histogram_table.size(); i++) { - Rectangle rect = getLargestRectangleInRow(histogram_table[i], i, input.size()); - if (rect.area > largest_rectangle.area) { - largest_rectangle = rect; - } - } - return largest_rectangle; -} - -boost::optional getLastExtendedPoint( - const autoware_auto_planning_msgs::msg::PathPoint & path_point, - const geometry_msgs::msg::Pose & pose, const double delta_yaw_threshold, - const double delta_dist_threshold) -{ - const double dist = calculate2DDistance(path_point.pose.position, pose.position); - const double diff_yaw = tf2::getYaw(path_point.pose.orientation) - tf2::getYaw(pose.orientation); - const double norm_diff_yaw = normalizeRadian(diff_yaw); - if ( - dist > 1e-6 && dist < delta_dist_threshold && std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - return path_point.pose.position; - } else { - return boost::none; - } -} - -boost::optional getLastExtendedTrajPoint( - const autoware_auto_planning_msgs::msg::PathPoint & path_point, - const geometry_msgs::msg::Pose & pose, const double delta_yaw_threshold, - const double delta_dist_threshold) -{ - const double dist = calculate2DDistance(path_point.pose.position, pose.position); - const double diff_yaw = tf2::getYaw(path_point.pose.orientation) - tf2::getYaw(pose.orientation); - const double norm_diff_yaw = normalizeRadian(diff_yaw); - if ( - dist > 1e-6 && dist < delta_dist_threshold && std::fabs(norm_diff_yaw) < delta_yaw_threshold) { - autoware_auto_planning_msgs::msg::TrajectoryPoint tmp_traj_p; - tmp_traj_p.pose.position = path_point.pose.position; - tmp_traj_p.pose.orientation = - util::getQuaternionFromPoints(path_point.pose.position, pose.position); - tmp_traj_p.longitudinal_velocity_mps = path_point.longitudinal_velocity_mps; - return tmp_traj_p; - } else { - return boost::none; - } -} - -std::vector getVehicleFootprints( - const std::vector & optimized_points, - const VehicleParam & vehicle_param) -{ - const double baselink_to_top = vehicle_param.length - vehicle_param.rear_overhang; - const double half_width = vehicle_param.width * 0.5; - std::vector rel_lon_offset{baselink_to_top, baselink_to_top, 0, 0}; - std::vector rel_lat_offset{half_width, -half_width, -half_width, half_width}; - std::vector rects; - for (std::size_t i = 0; i < optimized_points.size(); i++) { - // for (int i = nearest_idx; i < optimized_points.size(); i++) { - std::vector abs_points; - for (std::size_t j = 0; j < rel_lon_offset.size(); j++) { - geometry_msgs::msg::Point rel_point; - rel_point.x = rel_lon_offset[j]; - rel_point.y = rel_lat_offset[j]; - abs_points.push_back( - util::transformToAbsoluteCoordinate2D(rel_point, optimized_points[i].pose)); - } - Footprint rect; - rect.p = optimized_points[i].pose.position; - rect.top_left = abs_points[0]; - rect.top_right = abs_points[1]; - rect.bottom_right = abs_points[2]; - rect.bottom_left = abs_points[3]; - rects.push_back(rect); - } - return rects; -} - -/* - * calculate distance in x-y 2D space - */ -std::vector calcEuclidDist(const std::vector & x, const std::vector & y) -{ - if (x.size() != y.size()) { - std::cerr << "x y vector size should be the same." << std::endl; - } - - std::vector dist_v; - dist_v.push_back(0.0); - for (unsigned int i = 0; i < x.size() - 1; ++i) { - const double dx = x.at(i + 1) - x.at(i); - const double dy = y.at(i + 1) - y.at(i); - dist_v.push_back(dist_v.at(i) + std::hypot(dx, dy)); - } - - return dist_v; -} - -bool hasValidNearestPointFromEgo( - const geometry_msgs::msg::Pose & ego_pose, const Trajectories & trajs, - const TrajectoryParam & traj_param) -{ - const auto traj = concatTraj(trajs); - const auto interpolated_points = - util::getInterpolatedPoints(traj, traj_param.delta_arc_length_for_trajectory); - const int default_idx = -1; - const int nearest_idx_with_thres = util::getNearestIdx( - interpolated_points, ego_pose, default_idx, traj_param.delta_yaw_threshold_for_closest_point, - traj_param.delta_dist_threshold_for_closest_point); - if (nearest_idx_with_thres == default_idx) { - return false; - } - return true; -} - -std::vector concatTraj( - const Trajectories & trajs) -{ - std::vector trajectory; - trajectory.insert( - trajectory.end(), trajs.model_predictive_trajectory.begin(), - trajs.model_predictive_trajectory.end()); - trajectory.insert( - trajectory.end(), trajs.extended_trajectory.begin(), trajs.extended_trajectory.end()); - return trajectory; -} - -int getZeroVelocityIdx( - const bool is_showing_debug_info, const std::vector & fine_points, - const std::vector & path_points, - const std::unique_ptr & opt_trajs, const TrajectoryParam & traj_param) -{ - const int default_idx = fine_points.size() - 1; - const int zero_velocity_idx_from_path = - getZeroVelocityIdxFromPoints(path_points, fine_points, default_idx, traj_param); - - int zero_velocity_idx_from_opt_points = zero_velocity_idx_from_path; - if (opt_trajs) { - zero_velocity_idx_from_opt_points = getZeroVelocityIdxFromPoints( - util::concatTraj(*opt_trajs), fine_points, default_idx, traj_param); - } - RCLCPP_INFO_EXPRESSION( - rclcpp::get_logger("util"), is_showing_debug_info, - "0 m/s idx from path: %d from opt: %d total size %zu", zero_velocity_idx_from_path, - zero_velocity_idx_from_opt_points, fine_points.size()); - const int zero_velocity_idx = - std::min(zero_velocity_idx_from_path, zero_velocity_idx_from_opt_points); - - if (is_showing_debug_info) { - int zero_velocity_path_idx = path_points.size() - 1; - for (std::size_t i = 0; i < path_points.size(); i++) { - if (path_points[i].longitudinal_velocity_mps < 1e-6) { - zero_velocity_path_idx = i; - break; - } - } - const float debug_dist = util::calculate2DDistance( - path_points[zero_velocity_path_idx].pose.position, fine_points[zero_velocity_idx]); - RCLCPP_INFO( - rclcpp::get_logger("util"), "Dist from path 0 velocity point: = %f [m]", debug_dist); - } - return zero_velocity_idx; -} - -template -int getZeroVelocityIdxFromPoints( - const T & points, const std::vector & fine_points, - const int /* default_idx */, const TrajectoryParam & traj_param) -{ - int zero_velocity_points_idx = points.size() - 1; - for (std::size_t i = 0; i < points.size(); i++) { - if (points[i].longitudinal_velocity_mps < 1e-6) { - zero_velocity_points_idx = i; - break; - } - } - const int default_zero_velocity_fine_points_idx = fine_points.size() - 1; - const int zero_velocity_fine_points_idx = util::getNearestIdx( - fine_points, points[zero_velocity_points_idx].pose, default_zero_velocity_fine_points_idx, - traj_param.delta_yaw_threshold_for_closest_point, - traj_param.delta_dist_threshold_for_closest_point); - return zero_velocity_fine_points_idx; -} -template int getZeroVelocityIdxFromPoints>( - const std::vector &, - const std::vector &, const int, const TrajectoryParam &); -template int -getZeroVelocityIdxFromPoints>( - const std::vector &, - const std::vector &, const int, const TrajectoryParam &); - -template -double getArcLength(const T & points, const int initial_idx) -{ - double accum_arc_length = 0; - for (size_t i = initial_idx; i < points.size(); i++) { - if (i > 0) { - const double dist = - util::calculate2DDistance(points[i].pose.position, points[i - 1].pose.position); - accum_arc_length += dist; - } - } - return accum_arc_length; -} -template double getArcLength>( - const std::vector &, const int); -template double getArcLength>( - const std::vector &, const int); - -double getArcLength(const std::vector & points, const int initial_idx) -{ - double accum_arc_length = 0; - for (size_t i = initial_idx; i < points.size(); i++) { - if (i > 0) { - const double dist = util::calculate2DDistance(points[i].position, points[i - 1].position); - accum_arc_length += dist; - } - } - return accum_arc_length; -} - -void logOSQPSolutionStatus(const int solution_status) -{ - /****************** - * Solver Status * - ******************/ - int OSQP_DUAL_INFEASIBLE_INACCURATE = 4; - int OSQP_PRIMAL_INFEASIBLE_INACCURATE = 3; - int OSQP_SOLVED_INACCURATE = 2; - int OSQP_SOLVED = 1; - int OSQP_MAX_ITER_REACHED = -2; - int OSQP_PRIMAL_INFEASIBLE = -3; /* primal infeasible */ - int OSQP_DUAL_INFEASIBLE = -4; /* dual infeasible */ - int OSQP_SIGINT = -5; /* interrupted by user */ - - if (solution_status == OSQP_SOLVED) { - // RCLCPP_INFO(rclcpp::get_logger("util"),"[Avoidance] OSQP solution status: OSQP_SOLVED"); - } else if (solution_status == OSQP_DUAL_INFEASIBLE_INACCURATE) { - RCLCPP_WARN( - rclcpp::get_logger("util"), - "[Avoidance] OSQP solution status: OSQP_DUAL_INFEASIBLE_INACCURATE"); - } else if (solution_status == OSQP_PRIMAL_INFEASIBLE_INACCURATE) { - RCLCPP_WARN( - rclcpp::get_logger("util"), - "[Avoidance] OSQP solution status: OSQP_PRIMAL_INFEASIBLE_INACCURATE"); - } else if (solution_status == OSQP_SOLVED_INACCURATE) { - RCLCPP_WARN( - rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_SOLVED_INACCURATE"); - } else if (solution_status == OSQP_MAX_ITER_REACHED) { - RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_ITER_REACHED"); - } else if (solution_status == OSQP_PRIMAL_INFEASIBLE) { - RCLCPP_WARN( - rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_PRIMAL_INFEASIBLE"); - } else if (solution_status == OSQP_DUAL_INFEASIBLE) { - RCLCPP_WARN( - rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_DUAL_INFEASIBLE"); - } else if (solution_status == OSQP_SIGINT) { - RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_SIGINT"); - RCLCPP_WARN( - rclcpp::get_logger("util"), "[Avoidance] Interrupted by user, process will be finished."); - std::exit(0); - } else { - RCLCPP_WARN( - rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: Not defined %d", - solution_status); - } -} - -} // namespace util diff --git a/planning/obstacle_avoidance_planner/src/utils.cpp b/planning/obstacle_avoidance_planner/src/utils.cpp new file mode 100644 index 0000000000000..3ae8832531da5 --- /dev/null +++ b/planning/obstacle_avoidance_planner/src/utils.cpp @@ -0,0 +1,551 @@ +// Copyright 2020 Tier IV, Inc. +// +// 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 "obstacle_avoidance_planner/utils.hpp" + +#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" +#include "obstacle_avoidance_planner/mpt_optimizer.hpp" +#include "tf2/utils.h" + +#include "autoware_auto_planning_msgs/msg/path_point.hpp" +#include "autoware_auto_planning_msgs/msg/trajectory_point.hpp" +#include "geometry_msgs/msg/point32.hpp" +#include "geometry_msgs/msg/pose.hpp" +#include "nav_msgs/msg/map_meta_data.hpp" + +#include "boost/optional.hpp" + +#include +#include +#include +#include +#include + +namespace +{ +std::vector convertEulerAngleToMonotonic(const std::vector & angle) +{ + if (angle.empty()) { + return std::vector{}; + } + + std::vector monotonic_angle{angle.front()}; + for (size_t i = 1; i < angle.size(); ++i) { + const double diff_angle = angle.at(i) - monotonic_angle.back(); + monotonic_angle.push_back( + monotonic_angle.back() + tier4_autoware_utils::normalizeRadian(diff_angle)); + } + + return monotonic_angle; +} + +std::vector calcEuclidDist(const std::vector & x, const std::vector & y) +{ + if (x.size() != y.size()) { + std::cerr << "x y vector size should be the same." << std::endl; + } + + std::vector dist_v; + dist_v.push_back(0.0); + for (unsigned int i = 0; i < x.size() - 1; ++i) { + const double dx = x.at(i + 1) - x.at(i); + const double dy = y.at(i + 1) - y.at(i); + dist_v.push_back(dist_v.at(i) + std::hypot(dx, dy)); + } + + return dist_v; +} + +std::array, 3> validateTrajectoryPoints( + const std::vector & points) +{ + constexpr double epsilon = 1e-6; + + std::vector x; + std::vector y; + std::vector yaw; + for (size_t i = 0; i < points.size(); i++) { + if (i > 0) { + if ( + std::fabs(points[i].pose.position.x - points[i - 1].pose.position.x) < epsilon && + std::fabs(points[i].pose.position.y - points[i - 1].pose.position.y) < epsilon) { + continue; + } + } + x.push_back(points[i].pose.position.x); + y.push_back(points[i].pose.position.y); + yaw.push_back(tf2::getYaw(points[i].pose.orientation)); + } + return {x, y, yaw}; +} + +std::array, 2> validatePoints( + const std::vector & points) +{ + std::vector x; + std::vector y; + for (size_t i = 0; i < points.size(); i++) { + if (i > 0) { + if ( + std::fabs(points[i].pose.position.x - points[i - 1].pose.position.x) < 1e-6 && + std::fabs(points[i].pose.position.y - points[i - 1].pose.position.y) < 1e-6) { + continue; + } + } + x.push_back(points[i].pose.position.x); + y.push_back(points[i].pose.position.y); + } + return {x, y}; +} + +// only two points is supported +std::vector slerpTwoPoints( + std::vector base_s, std::vector base_x, const double begin_diff, + const double end_diff, std::vector new_s) +{ + const double h = base_s.at(1) - base_s.at(0); + + const double c = begin_diff; + const double d = base_x.at(0); + const double a = (end_diff * h - 2 * base_x.at(1) + c * h + 2 * d) / std::pow(h, 3); + const double b = (3 * base_x.at(1) - end_diff * h - 2 * c * h - 3 * d) / std::pow(h, 2); + + std::vector res; + for (const auto & s : new_s) { + const double ds = s - base_s.at(0); + res.push_back(d + (c + (b + a * ds) * ds) * ds); + } + + return res; +} +} // namespace + +namespace tier4_autoware_utils +{ +template <> +geometry_msgs::msg::Point getPoint(const ReferencePoint & p) +{ + return p.p; +} + +template <> +geometry_msgs::msg::Pose getPose(const ReferencePoint & p) +{ + geometry_msgs::msg::Pose pose; + pose.position = p.p; + pose.orientation = createQuaternionFromYaw(p.yaw); + return pose; +} +} // namespace tier4_autoware_utils + +namespace geometry_utils +{ +geometry_msgs::msg::Point transformToAbsoluteCoordinate2D( + const geometry_msgs::msg::Point & point, const geometry_msgs::msg::Pose & origin) +{ + // NOTE: implement transformation without defining yaw variable + // but directly sin/cos of yaw for fast calculation + const auto & q = origin.orientation; + const double cos_yaw = 1 - 2 * q.z * q.z; + const double sin_yaw = 2 * q.w * q.z; + + geometry_msgs::msg::Point absolute_p; + absolute_p.x = point.x * cos_yaw - point.y * sin_yaw + origin.position.x; + absolute_p.y = point.x * sin_yaw + point.y * cos_yaw + origin.position.y; + absolute_p.z = point.z; + + return absolute_p; +} + +geometry_msgs::msg::Quaternion getQuaternionFromPoints( + const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & a_root) +{ + const double yaw = tier4_autoware_utils::calcAzimuthAngle(a_root, a); + return tier4_autoware_utils::createQuaternionFromYaw(yaw); +} + +geometry_msgs::msg::Quaternion getQuaternionFromPoints( + const geometry_msgs::msg::Point & p1, const geometry_msgs::msg::Point & p2, + const geometry_msgs::msg::Point & p3, const geometry_msgs::msg::Point & p4) +{ + const double dx = (8.0 * (p3.x - p2.x) - (p4.x - p1.x)) / 12.0; + const double dy = (8.0 * (p3.y - p2.y) - (p4.y - p1.y)) / 12.0; + const double yaw = std::atan2(dy, dx); + + return tier4_autoware_utils::createQuaternionFromYaw(yaw); +} + +boost::optional transformMapToOptionalImage( + const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & occupancy_grid_info) +{ + const geometry_msgs::msg::Point relative_p = + transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); + const double resolution = occupancy_grid_info.resolution; + const double map_y_height = occupancy_grid_info.height; + const double map_x_width = occupancy_grid_info.width; + const double map_x_in_image_resolution = relative_p.x / resolution; + const double map_y_in_image_resolution = relative_p.y / resolution; + const double image_x = map_y_height - map_y_in_image_resolution; + const double image_y = map_x_width - map_x_in_image_resolution; + if ( + image_x >= 0 && image_x < static_cast(map_y_height) && image_y >= 0 && + image_y < static_cast(map_x_width)) { + geometry_msgs::msg::Point image_point; + image_point.x = image_x; + image_point.y = image_y; + return image_point; + } else { + return boost::none; + } +} + +bool transformMapToImage( + const geometry_msgs::msg::Point & map_point, + const nav_msgs::msg::MapMetaData & occupancy_grid_info, geometry_msgs::msg::Point & image_point) +{ + geometry_msgs::msg::Point relative_p = + transformToRelativeCoordinate2D(map_point, occupancy_grid_info.origin); + const double map_y_height = occupancy_grid_info.height; + const double map_x_width = occupancy_grid_info.width; + const double scale = 1 / occupancy_grid_info.resolution; + const double map_x_in_image_resolution = relative_p.x * scale; + const double map_y_in_image_resolution = relative_p.y * scale; + const double image_x = map_y_height - map_y_in_image_resolution; + const double image_y = map_x_width - map_x_in_image_resolution; + if ( + image_x >= 0 && image_x < static_cast(map_y_height) && image_y >= 0 && + image_y < static_cast(map_x_width)) { + image_point.x = image_x; + image_point.y = image_y; + return true; + } else { + return false; + } +} +} // namespace geometry_utils + +namespace interpolation_utils +{ +std::vector interpolate2DPoints( + const std::vector & base_x, const std::vector & base_y, const double resolution, + const double offset = 0.0) +{ + if (base_x.empty() || base_y.empty()) { + return std::vector{}; + } + const std::vector base_s = calcEuclidDist(base_x, base_y); + if (base_s.empty() || base_s.size() == 1) { + return std::vector{}; + } + + std::vector new_s; + for (double i = offset; i < base_s.back() - 1e-6; i += resolution) { + new_s.push_back(i); + } + if (new_s.empty()) { + return std::vector{}; + } + + // spline interpolation + const std::vector interpolated_x = interpolation::slerp(base_s, base_x, new_s); + const std::vector interpolated_y = interpolation::slerp(base_s, base_y, new_s); + for (size_t i = 0; i < interpolated_x.size(); ++i) { + if (std::isnan(interpolated_x[i]) || std::isnan(interpolated_y[i])) { + return std::vector{}; + } + } + + std::vector interpolated_points; + for (size_t i = 0; i < interpolated_x.size(); ++i) { + geometry_msgs::msg::Point point; + point.x = interpolated_x[i]; + point.y = interpolated_y[i]; + interpolated_points.push_back(point); + } + + return interpolated_points; +} + +std::vector interpolateConnected2DPoints( + const std::vector & base_x, const std::vector & base_y, const double resolution, + const double begin_yaw, const double end_yaw) +{ + if (base_x.empty() || base_y.empty()) { + return std::vector{}; + } + std::vector base_s = calcEuclidDist(base_x, base_y); + if (base_s.empty() || base_s.size() == 1) { + return std::vector{}; + } + std::vector new_s; + for (double i = 0.0; i < base_s.back() - 1e-6; i += resolution) { + new_s.push_back(i); + } + + // spline interpolation + const auto interpolated_x = + slerpTwoPoints(base_s, base_x, std::cos(begin_yaw), std::cos(end_yaw), new_s); + const auto interpolated_y = + slerpTwoPoints(base_s, base_y, std::sin(begin_yaw), std::sin(end_yaw), new_s); + + for (size_t i = 0; i < interpolated_x.size(); i++) { + if (std::isnan(interpolated_x[i]) || std::isnan(interpolated_y[i])) { + return std::vector{}; + } + } + + std::vector interpolated_points; + for (size_t i = 0; i < interpolated_x.size(); i++) { + autoware_auto_planning_msgs::msg::TrajectoryPoint point; + point.pose.position.x = interpolated_x[i]; + point.pose.position.y = interpolated_y[i]; + + const size_t front_idx = (i == interpolated_x.size() - 1) ? i - 1 : i; + const double dx = interpolated_x[front_idx + 1] - interpolated_x[front_idx]; + const double dy = interpolated_y[front_idx + 1] - interpolated_y[front_idx]; + const double yaw = std::atan2(dy, dx); + point.pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(yaw); + + interpolated_points.push_back(point); + } + + return interpolated_points; +} + +std::vector interpolate2DTrajectoryPoints( + const std::vector & base_x, const std::vector & base_y, + const std::vector & base_yaw, const double resolution) +{ + if (base_x.empty() || base_y.empty() || base_yaw.empty()) { + return std::vector{}; + } + std::vector base_s = calcEuclidDist(base_x, base_y); + if (base_s.empty() || base_s.size() == 1) { + return std::vector{}; + } + std::vector new_s; + for (double i = 0.0; i < base_s.back() - 1e-6; i += resolution) { + new_s.push_back(i); + } + + const auto monotonic_base_yaw = convertEulerAngleToMonotonic(base_yaw); + + // spline interpolation + const auto interpolated_x = interpolation::slerp(base_s, base_x, new_s); + const auto interpolated_y = interpolation::slerp(base_s, base_y, new_s); + const auto interpolated_yaw = interpolation::slerp(base_s, monotonic_base_yaw, new_s); + + for (size_t i = 0; i < interpolated_x.size(); i++) { + if (std::isnan(interpolated_x[i]) || std::isnan(interpolated_y[i])) { + return std::vector{}; + } + } + + std::vector interpolated_points; + for (size_t i = 0; i < interpolated_x.size(); i++) { + autoware_auto_planning_msgs::msg::TrajectoryPoint point; + point.pose.position.x = interpolated_x[i]; + point.pose.position.y = interpolated_y[i]; + point.pose.orientation = tier4_autoware_utils::createQuaternionFromYaw( + tier4_autoware_utils::normalizeRadian(interpolated_yaw[i])); + interpolated_points.push_back(point); + } + + return interpolated_points; +} + +std::vector getInterpolatedTrajectoryPoints( + const std::vector & points, + const double delta_arc_length) +{ + std::array, 3> validated_pose = validateTrajectoryPoints(points); + return interpolation_utils::interpolate2DTrajectoryPoints( + validated_pose.at(0), validated_pose.at(1), validated_pose.at(2), delta_arc_length); +} + +std::vector getConnectedInterpolatedPoints( + const std::vector & points, + const double delta_arc_length, const double begin_yaw, const double end_yaw) +{ + std::array, 2> validated_pose = validatePoints(points); + return interpolation_utils::interpolateConnected2DPoints( + validated_pose.at(0), validated_pose.at(1), delta_arc_length, begin_yaw, end_yaw); +} +} // namespace interpolation_utils + +namespace points_utils +{ +// functions to convert to another type of points +std::vector convertToPosesWithYawEstimation( + const std::vector points) +{ + std::vector poses; + if (points.empty()) { + return poses; + } else if (points.size() == 1) { + geometry_msgs::msg::Pose pose; + pose.position = points.at(0); + poses.push_back(pose); + return poses; + } + + for (size_t i = 0; i < points.size(); ++i) { + geometry_msgs::msg::Pose pose; + pose.position = points.at(i); + + const size_t front_idx = (i == points.size() - 1) ? i - 1 : i; + const double points_yaw = + tier4_autoware_utils::calcAzimuthAngle(points.at(front_idx), points.at(front_idx + 1)); + pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(points_yaw); + + poses.push_back(pose); + } + return poses; +} + +template +ReferencePoint convertToReferencePoint(const T & point) +{ + ReferencePoint ref_point; + + const auto & pose = tier4_autoware_utils::getPose(point); + ref_point.p = pose.position; + ref_point.yaw = tf2::getYaw(pose.orientation); + + return ref_point; +} + +template ReferencePoint convertToReferencePoint( + const autoware_auto_planning_msgs::msg::TrajectoryPoint & point); +template ReferencePoint convertToReferencePoint( + const geometry_msgs::msg::Pose & point); +template <> +ReferencePoint convertToReferencePoint(const geometry_msgs::msg::Point & point) +{ + ReferencePoint ref_point; + + ref_point.p = point; + + return ref_point; +} + +std::vector concatTrajectory( + const std::vector & traj_points, + const std::vector & extended_traj_points) +{ + std::vector trajectory; + trajectory.insert(trajectory.end(), traj_points.begin(), traj_points.end()); + trajectory.insert(trajectory.end(), extended_traj_points.begin(), extended_traj_points.end()); + return trajectory; +} + +void compensateLastPose( + const autoware_auto_planning_msgs::msg::PathPoint & last_path_point, + std::vector & traj_points, + const double delta_dist_threshold, const double delta_yaw_threshold) +{ + if (traj_points.empty()) { + traj_points.push_back(convertToTrajectoryPoint(last_path_point)); + return; + } + + const geometry_msgs::msg::Pose last_traj_pose = traj_points.back().pose; + + const double dist = + tier4_autoware_utils::calcDistance2d(last_path_point.pose.position, last_traj_pose.position); + const double norm_diff_yaw = [&]() { + const double diff_yaw = + tf2::getYaw(last_path_point.pose.orientation) - tf2::getYaw(last_traj_pose.orientation); + return tier4_autoware_utils::normalizeRadian(diff_yaw); + }(); + if (dist > delta_dist_threshold || std::fabs(norm_diff_yaw) > delta_yaw_threshold) { + traj_points.push_back(convertToTrajectoryPoint(last_path_point)); + } +} + +int getNearestIdx( + const std::vector & points, const double target_s, const int begin_idx) +{ + double nearest_delta_s = std::numeric_limits::max(); + int nearest_idx = begin_idx; + for (size_t i = begin_idx; i < points.size(); i++) { + double diff = std::fabs(target_s - points[i].s); + if (diff < nearest_delta_s) { + nearest_delta_s = diff; + nearest_idx = i; + } + } + return nearest_idx; +} +} // namespace points_utils + +namespace utils +{ +void logOSQPSolutionStatus(const int solution_status) +{ + /****************** + * Solver Status * + ******************/ + const int LOCAL_OSQP_SOLVED = 1; + const int LOCAL_OSQP_SOLVED_INACCURATE = 2; + const int LOCAL_OSQP_MAX_ITER_REACHED = -2; + const int LOCAL_OSQP_PRIMAL_INFEASIBLE = -3; + const int LOCAL_OSQP_PRIMAL_INFEASIBLE_INACCURATE = 3; + const int LOCAL_OSQP_DUAL_INFEASIBLE = -4; + const int LOCAL_OSQP_DUAL_INFEASIBLE_INACCURATE = 4; + const int LOCAL_OSQP_SIGINT = -5; + const int LOCAL_OSQP_TIME_LIMIT_REACHED = -6; + const int LOCAL_OSQP_UNSOLVED = -10; + const int LOCAL_OSQP_NON_CVX = -7; + + if (solution_status == LOCAL_OSQP_SOLVED) { + } else if (solution_status == LOCAL_OSQP_DUAL_INFEASIBLE_INACCURATE) { + RCLCPP_WARN( + rclcpp::get_logger("util"), + "[Avoidance] OSQP solution status: OSQP_DUAL_INFEASIBLE_INACCURATE"); + } else if (solution_status == LOCAL_OSQP_PRIMAL_INFEASIBLE_INACCURATE) { + RCLCPP_WARN( + rclcpp::get_logger("util"), + "[Avoidance] OSQP solution status: OSQP_PRIMAL_INFEASIBLE_INACCURATE"); + } else if (solution_status == LOCAL_OSQP_SOLVED_INACCURATE) { + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_SOLVED_INACCURATE"); + } else if (solution_status == LOCAL_OSQP_MAX_ITER_REACHED) { + RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_ITER_REACHED"); + } else if (solution_status == LOCAL_OSQP_PRIMAL_INFEASIBLE) { + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_PRIMAL_INFEASIBLE"); + } else if (solution_status == LOCAL_OSQP_DUAL_INFEASIBLE) { + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_DUAL_INFEASIBLE"); + } else if (solution_status == LOCAL_OSQP_SIGINT) { + RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_SIGINT"); + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] Interrupted by user, process will be finished."); + std::exit(0); + } else if (solution_status == LOCAL_OSQP_TIME_LIMIT_REACHED) { + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_TIME_LIMIT_REACHED"); + } else if (solution_status == LOCAL_OSQP_UNSOLVED) { + RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_UNSOLVED"); + } else if (solution_status == LOCAL_OSQP_NON_CVX) { + RCLCPP_WARN(rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: OSQP_NON_CVX"); + } else { + RCLCPP_WARN( + rclcpp::get_logger("util"), "[Avoidance] OSQP solution status: Not defined %d", + solution_status); + } +} +} // namespace utils diff --git a/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_bicycle_kinematics.cpp b/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_bicycle_kinematics.cpp index d842036dd6f97..e752c31f242da 100644 --- a/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_bicycle_kinematics.cpp +++ b/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_bicycle_kinematics.cpp @@ -15,48 +15,53 @@ #include "obstacle_avoidance_planner/vehicle_model/vehicle_model_bicycle_kinematics.hpp" #include +#include +#include -KinematicsBicycleModel::KinematicsBicycleModel( - const double wheelbase, const double steer_lim, const double steer_tau) -: VehicleModelInterface(/* dim_x */ 3, /* dim_u */ 1, /* dim_y */ 2) +KinematicsBicycleModel::KinematicsBicycleModel(const double wheel_base, const double steer_limit) +: VehicleModelInterface(/* dim_x */ 2, /* dim_u */ 1, /* dim_y */ 2, wheel_base, steer_limit) { - wheelbase_ = wheelbase; - steer_lim_ = steer_lim; - steer_tau_ = steer_tau; } -void KinematicsBicycleModel::calculateDiscreteMatrix( - Eigen::MatrixXd * Ad, Eigen::MatrixXd * Bd, Eigen::MatrixXd * Cd, Eigen::MatrixXd * Wd, - const double ds) +void KinematicsBicycleModel::calculateStateEquationMatrix( + Eigen::MatrixXd & Ad, Eigen::MatrixXd & Bd, Eigen::MatrixXd & Wd, const double ds) { - auto sign = [](double x) { return (x > 0.0) - (x < 0.0); }; + // const double epsilon = std::numeric_limits::epsilon(); + // constexpr double dt = 0.03; // assuming delta time for steer tau - /* Linearize delta around delta_r (reference delta) */ - double delta_r = atan(wheelbase_ * curvature_); - if (abs(delta_r) >= steer_lim_) { - delta_r = steer_lim_ * static_cast(sign(delta_r)); - } - double cos_delta_r_squared_inv = 1 / (cos(delta_r) * cos(delta_r)); + /* + const double lf = wheel_base_ - center_offset_from_base_; + const double lr = center_offset_from_base_; + */ - // Ad << 0.0, velocity, 0.0, 0.0, 0.0, velocity / wheelbase_ * cos_delta_r_squared_inv, 0.0, 0.0, - // -1.0 / steer_tau_; - // Eigen::MatrixXd I = Eigen::MatrixXd::Identity(dim_x_, dim_x_); - // Ad = (I - dt * 0.5 * Ad).inverse() * (I + dt * 0.5 * Ad); // bilinear discretization + const double delta_r = std::atan(wheel_base_ * curvature_); + const double cropped_delta_r = std::clamp(delta_r, -steer_limit_, steer_limit_); - // assuming delta time for steer tau - constexpr double dt = 0.03; - *Ad << 1.0, ds, 0, 0.0, 1, ds / (wheelbase_ * cos_delta_r_squared_inv), 0.0, 0, - 1 - dt / steer_tau_; + // NOTE: cos(delta_r) will not be zero since that happens only when curvature is infinity + Ad << 1.0, ds, // + 0.0, 1.0; - *Bd << 0.0, 0.0, dt / steer_tau_; + Bd << 0.0, ds / wheel_base_ / std::pow(std::cos(delta_r), 2.0); - *Cd << 1.0, 0.0, 0.0, 0.0, 1.0, 0.0; + Wd << 0.0, -ds * curvature_ + ds / wheel_base_ * + (std::tan(cropped_delta_r) - + cropped_delta_r / std::pow(std::cos(cropped_delta_r), 2.0)); +} - *Wd << 0.0, - -ds * curvature_ + ds / wheelbase_ * (tan(delta_r) - delta_r * cos_delta_r_squared_inv), 0.0; +void KinematicsBicycleModel::calculateObservationMatrix(Eigen::MatrixXd & Cd) +{ + Cd << 1.0, 0.0, 0.0, 0.0, 1.0, 0.0; +} + +void KinematicsBicycleModel::calculateObservationSparseMatrix( + std::vector> & Cd_vec) +{ + Cd_vec.clear(); + Cd_vec.push_back({0, 0, 1.0}); + Cd_vec.push_back({1, 1, 1.0}); } void KinematicsBicycleModel::calculateReferenceInput(Eigen::MatrixXd * Uref) { - (*Uref)(0, 0) = std::atan(wheelbase_ * curvature_); + (*Uref)(0, 0) = std::atan(wheel_base_ * curvature_); } diff --git a/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_interface.cpp b/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_interface.cpp index 50a2d72f841a6..8e6522725c133 100644 --- a/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_interface.cpp +++ b/planning/obstacle_avoidance_planner/src/vehicle_model/vehicle_model_interface.cpp @@ -14,11 +14,24 @@ #include "obstacle_avoidance_planner/vehicle_model/vehicle_model_interface.hpp" -VehicleModelInterface::VehicleModelInterface(int dim_x, int dim_u, int dim_y) -: dim_x_(dim_x), dim_u_(dim_u), dim_y_(dim_y) +VehicleModelInterface::VehicleModelInterface( + int dim_x, int dim_u, int dim_y, double wheel_base, double steer_limit) +: dim_x_(dim_x), + dim_u_(dim_u), + dim_y_(dim_y), + wheel_base_(wheel_base), + steer_limit_(steer_limit), + center_offset_from_base_(0.0) { } + int VehicleModelInterface::getDimX() { return dim_x_; } int VehicleModelInterface::getDimU() { return dim_u_; } int VehicleModelInterface::getDimY() { return dim_y_; } + +void VehicleModelInterface::updateCenterOffset(const double center_offset_from_base) +{ + center_offset_from_base_ = center_offset_from_base; +} + void VehicleModelInterface::setCurvature(const double curvature) { curvature_ = curvature; } From 3108a010d6c7d09d051372f47355966d9e4e67ab Mon Sep 17 00:00:00 2001 From: Takayuki Murooka Date: Fri, 18 Mar 2022 10:35:37 +0900 Subject: [PATCH 09/16] feat(system_error_monitor): output emergency reason for debugging (#510) * feat(system_error_monitor): output emergency reason for debugging Signed-off-by: Takayuki Murooka * print all emergency reasons with 5 sec throttle using macros Signed-off-by: Takayuki Murooka * not use macros Signed-off-by: Takayuki Murooka * use const std::shared_ptr for argument of clock Signed-off-by: Takayuki Murooka * use enum class for DebugLevel Signed-off-by: Takayuki Murooka * resolved review's comment Signed-off-by: Takayuki Murooka --- .../src/system_error_monitor_core.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/system/system_error_monitor/src/system_error_monitor_core.cpp b/system/system_error_monitor/src/system_error_monitor_core.cpp index 12fdafce41708..1a0b696bb36f4 100644 --- a/system/system_error_monitor/src/system_error_monitor_core.cpp +++ b/system/system_error_monitor/src/system_error_monitor_core.cpp @@ -28,6 +28,35 @@ namespace { +enum class DebugLevel { DEBUG, INFO, WARN, ERROR, FATAL }; + +template +void logThrottledNamed( + const std::string & logger_name, const rclcpp::Clock::SharedPtr clock, const double duration_ms, + const std::string & message) +{ + static std::unordered_map last_output_time; + if (last_output_time.count(logger_name) != 0) { + const auto time_from_last_output = clock->now() - last_output_time.at(logger_name); + if (time_from_last_output.seconds() * 1000.0 < duration_ms) { + return; + } + } + + last_output_time[logger_name] = clock->now(); + if constexpr (debug_level == DebugLevel::DEBUG) { + RCLCPP_DEBUG(rclcpp::get_logger(logger_name), message.c_str()); + } else if constexpr (debug_level == DebugLevel::INFO) { + RCLCPP_INFO(rclcpp::get_logger(logger_name), message.c_str()); + } else if constexpr (debug_level == DebugLevel::WARN) { + RCLCPP_WARN(rclcpp::get_logger(logger_name), message.c_str()); + } else if constexpr (debug_level == DebugLevel::ERROR) { + RCLCPP_ERROR(rclcpp::get_logger(logger_name), message.c_str()); + } else if constexpr (debug_level == DebugLevel::FATAL) { + RCLCPP_FATAL(rclcpp::get_logger(logger_name), message.c_str()); + } +} + std::vector split(const std::string & str, const char delim) { std::vector elems; @@ -111,9 +140,17 @@ diagnostic_msgs::msg::DiagnosticArray convertHazardStatusToDiagnosticArray( diag_array.status.push_back(decorateDiag(hazard_diag, "[Safe Fault]")); } for (const auto & hazard_diag : hazard_status.diag_latent_fault) { + const std::string logger_name = "system_error_monitor " + hazard_diag.name; + logThrottledNamed( + logger_name, clock, 5000, "[Latent Fault]: " + hazard_diag.message); + diag_array.status.push_back(decorateDiag(hazard_diag, "[Latent Fault]")); } for (const auto & hazard_diag : hazard_status.diag_single_point_fault) { + const std::string logger_name = "system_error_monitor " + hazard_diag.name; + logThrottledNamed( + logger_name, clock, 5000, "[Single Point Fault]: " + hazard_diag.message); + diag_array.status.push_back(decorateDiag(hazard_diag, "[Single Point Fault]")); } From d9460e5040777899a614fe4ddbfb88f251b2cb69 Mon Sep 17 00:00:00 2001 From: Yutaka Shimizu <43805014+purewater0901@users.noreply.github.com> Date: Fri, 18 Mar 2022 12:57:04 +0900 Subject: [PATCH 10/16] feat(map_based_prediction): refactoring map based prediction (#491) * udpate map based prediction * fix CMakeLists.txt * fix format * clean code * use autoware utils in debug.cpp funcion * use autoware utils function * use offset pose * clean generate straight line function * clean calculate object yaw function * update readme * clean code * add several const variables * add new folder * add compile option * udpate map based prediction * fix CMakeLists.txt * fix format * clean code * use autoware utils in debug.cpp funcion * use autoware utils function * use offset pose * clean generate straight line function * clean calculate object yaw function * update readme * clean code * add several const variables * add new folder * add compile option * remove unnecessary arguments Signed-off-by: yutaka * fix confidence value * add block for minus velocity obstacle --- .../map_based_prediction/CMakeLists.txt | 8 +- .../{README.md => Readme.md} | 2 + .../include/cubic_spline.hpp | 257 ----- .../include/map_based_prediction.hpp | 86 -- .../map_based_prediction_node.hpp | 200 ++++ .../map_based_prediction/path_generator.hpp | 95 ++ .../include/map_based_prediction_ros.hpp | 164 --- .../launch/map_based_prediction.launch.xml | 10 +- perception/map_based_prediction/package.xml | 2 + perception/map_based_prediction/src/debug.cpp | 49 + .../src/map_based_prediction.cpp | 303 ------ .../src/map_based_prediction_node.cpp | 999 ++++++++++++++++++ .../src/map_based_prediction_ros.cpp | 760 ------------- .../src/path_generator.cpp | 305 ++++++ 14 files changed, 1665 insertions(+), 1575 deletions(-) rename perception/map_based_prediction/{README.md => Readme.md} (94%) delete mode 100644 perception/map_based_prediction/include/cubic_spline.hpp delete mode 100644 perception/map_based_prediction/include/map_based_prediction.hpp create mode 100644 perception/map_based_prediction/include/map_based_prediction/map_based_prediction_node.hpp create mode 100644 perception/map_based_prediction/include/map_based_prediction/path_generator.hpp delete mode 100644 perception/map_based_prediction/include/map_based_prediction_ros.hpp create mode 100644 perception/map_based_prediction/src/debug.cpp delete mode 100644 perception/map_based_prediction/src/map_based_prediction.cpp create mode 100644 perception/map_based_prediction/src/map_based_prediction_node.cpp delete mode 100644 perception/map_based_prediction/src/map_based_prediction_ros.cpp create mode 100644 perception/map_based_prediction/src/path_generator.cpp diff --git a/perception/map_based_prediction/CMakeLists.txt b/perception/map_based_prediction/CMakeLists.txt index 86de43c839ce6..1e290e43a6330 100644 --- a/perception/map_based_prediction/CMakeLists.txt +++ b/perception/map_based_prediction/CMakeLists.txt @@ -6,6 +6,7 @@ if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) endif() + if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Werror) endif() @@ -16,12 +17,13 @@ ament_auto_find_build_dependencies() find_package(Eigen3 REQUIRED) ament_auto_add_library(map_based_prediction_node SHARED - src/map_based_prediction_ros.cpp - src/map_based_prediction.cpp + src/map_based_prediction_node.cpp + src/path_generator.cpp + src/debug.cpp ) rclcpp_components_register_node(map_based_prediction_node - PLUGIN "map_based_prediction::MapBasedPredictionROS" + PLUGIN "map_based_prediction::MapBasedPredictionNode" EXECUTABLE map_based_prediction ) diff --git a/perception/map_based_prediction/README.md b/perception/map_based_prediction/Readme.md similarity index 94% rename from perception/map_based_prediction/README.md rename to perception/map_based_prediction/Readme.md index ee43ee89c81b1..4b436bf9c1e00 100644 --- a/perception/map_based_prediction/README.md +++ b/perception/map_based_prediction/Readme.md @@ -50,6 +50,7 @@ | Parameter | Type | Description | | ------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------ | +| `enable_delay_compensation` | bool | flag to enable the time delay compensation for the position of the object | | `prediction_time_horizon` | double | predict time duration for predicted path [s] | | `prediction_sampling_delta_time` | double | sampling time for points in predicted path [s] | | `min_velocity_for_map_based_prediction` | double | apply map-based prediction to the objects with higher velocity than this value | @@ -57,6 +58,7 @@ | `delta_yaw_threshold_for_searching_lanelet` | double | The threshold of the distance used when searching for the lane to which the object belongs [m] | | `sigma_lateral_offset` | double | Standard deviation for lateral position of objects [m] | | `sigma_yaw_angle` | double | Standard deviation yaw angle of objects [rad] | +| `object_buffer_time_length` | double | Time span of object history to store the information [s] | | `history_time_length` | double | Time span of object information used for prediction [s] | | `dist_ratio_threshold_to_left_bound` | double | Conditions for using lane change detection of objects. Distance to the left bound of lanelet. | | `dist_ratio_threshold_to_right_bound` | double | Conditions for using lane change detection of objects. Distance to the right bound of lanelet. | diff --git a/perception/map_based_prediction/include/cubic_spline.hpp b/perception/map_based_prediction/include/cubic_spline.hpp deleted file mode 100644 index d55f8ee9dde43..0000000000000 --- a/perception/map_based_prediction/include/cubic_spline.hpp +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2018 Forrest -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#ifndef CUBIC_SPLINE_HPP_ -#define CUBIC_SPLINE_HPP_ - -#include - -#include -#include -#include -#include -#include -#include - -namespace map_based_prediction -{ -static std::vector vec_diff(const std::vector & input) -{ - std::vector output; - for (unsigned int i = 1; i < input.size(); i++) { - output.push_back(input[i] - input[i - 1]); - } - return output; -} - -static std::vector cum_sum(const std::vector & input) -{ - std::vector output; - double temp = 0; - for (unsigned int i = 0; i < input.size(); i++) { - temp += input[i]; - output.push_back(temp); - } - return output; -} - -class Spline -{ -public: - std::vector x; - std::vector y; - int nx; - std::vector h; - std::vector a; - std::vector b; - std::vector c; - // Eigen::VectorXf c; - std::vector d; - - Spline() {} - // d_i * (x-x_i)^3 + c_i * (x-x_i)^2 + b_i * (x-x_i) + a_i - Spline(const std::vector & x_, const std::vector & y_) - : x(x_), y(y_), nx(x_.size()), h(vec_diff(x_)), a(y_) - { - Eigen::MatrixXd A = calc_A(); - Eigen::VectorXd B = calc_B(); - // std::cerr << "A det " << A.determinant() << std::endl; - // std::cerr << "A QR" << A.colPivHouseholderQr().solve(B) << std::endl; - Eigen::VectorXd c_eigen = A.colPivHouseholderQr().solve(B); - // std::cerr << "c eigen " << c_eigen << std::endl; - double * c_pointer = c_eigen.data(); - c.assign(c_pointer, c_pointer + c_eigen.rows()); - - for (int i = 0; i < nx - 1; i++) { - d.push_back((c[i + 1] - c[i]) / (3.0 * h[i])); - b.push_back((a[i + 1] - a[i]) / h[i] - h[i] * (c[i + 1] + 2 * c[i]) / 3.0); - } - } - - double calc(double t) - { - if (t < x.front() || t > x.back()) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx); - double dx = t - x[seg_id]; - return a[seg_id] + b[seg_id] * dx + c[seg_id] * dx * dx + d[seg_id] * dx * dx * dx; - } - - double calc(double t, double s) - { - if (t < 0 || t > s) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx); - double dx = t - x[seg_id]; - return a[seg_id] + b[seg_id] * dx + c[seg_id] * dx * dx + d[seg_id] * dx * dx * dx; - } - - double calc_d(double t) - { - if (t < x.front() || t > x.back()) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx - 1); - double dx = t - x[seg_id]; - return b[seg_id] + 2 * c[seg_id] * dx + 3 * d[seg_id] * dx * dx; - } - - double calc_d(double t, double s) - { - if (t < 0 || t > s) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx - 1); - double dx = t - x[seg_id]; - return b[seg_id] + 2 * c[seg_id] * dx + 3 * d[seg_id] * dx * dx; - } - - double calc_dd(double t) - { - if (t < x.front() || t > x.back()) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx); - double dx = t - x[seg_id]; - return 2 * c[seg_id] + 6 * d[seg_id] * dx; - } - - double calc_dd(double t, double s) - { - if (t < 0.0 || t > s) { - std::cout << "Dangerous" << std::endl; - std::cout << t << std::endl; - throw std::invalid_argument("received value out of the pre-defined range"); - } - int seg_id = bisect(t, 0, nx); - double dx = t - x[seg_id]; - return 2 * c[seg_id] + 6 * d[seg_id] * dx; - } - -private: - Eigen::MatrixXd calc_A() - { - Eigen::MatrixXd A = Eigen::MatrixXd::Zero(nx, nx); - A(0, 0) = 1; - for (int i = 0; i < nx - 1; i++) { - if (i != nx - 2) { - A(i + 1, i + 1) = 2 * (h[i] + h[i + 1]); - } - A(i + 1, i) = h[i]; - A(i, i + 1) = h[i]; - } - A(0, 1) = 0.0; - A(nx - 1, nx - 2) = 0.0; - A(nx - 1, nx - 1) = 1.0; - return A; - } - Eigen::VectorXd calc_B() - { - Eigen::VectorXd B = Eigen::VectorXd::Zero(nx); - for (int i = 0; i < nx - 2; i++) { - B(i + 1) = 3.0 * (a[i + 2] - a[i + 1]) / h[i + 1] - 3.0 * (a[i + 1] - a[i]) / h[i]; - } - return B; - } - - int bisect(double t, int start, int end) - { - int mid = (start + end) / 2; - if (t == x[mid] || end - start <= 1) { - return mid; - } else if (t > x[mid]) { - return bisect(t, mid, end); - } else { - return bisect(t, start, mid); - } - } -}; - -class Spline2D -{ -public: - Spline sx; - Spline sy; - std::vector s; - double max_s_value_; - - Spline2D(const std::vector & x, const std::vector & y) - { - s = calc_s(x, y); - sx = Spline(s, x); - sy = Spline(s, y); - max_s_value_ = *std::max_element(s.begin(), s.end()); - } - - std::array calc_position(double s_t) - { - double x = sx.calc(s_t, max_s_value_); - double y = sy.calc(s_t, max_s_value_); - return {{x, y}}; - } - - double calc_curvature(double s_t) - { - double dx = sx.calc_d(s_t, max_s_value_); - double ddx = sx.calc_dd(s_t, max_s_value_); - double dy = sy.calc_d(s_t, max_s_value_); - double ddy = sy.calc_dd(s_t, max_s_value_); - return (ddy * dx - ddx * dy) / (dx * dx + dy * dy); - } - - double calc_yaw(double s_t) - { - double dx = sx.calc_d(s_t, max_s_value_); - double dy = sy.calc_d(s_t, max_s_value_); - return std::atan2(dy, dx); - } - -private: - std::vector calc_s(const std::vector & x, const std::vector & y) - { - std::vector ds; - std::vector out_s{0}; - std::vector dx = vec_diff(x); - std::vector dy = vec_diff(y); - - for (unsigned int i = 0; i < dx.size(); i++) { - ds.push_back(std::sqrt(dx[i] * dx[i] + dy[i] * dy[i])); - } - - std::vector cum_ds = cum_sum(ds); - out_s.insert(out_s.end(), cum_ds.begin(), cum_ds.end()); - return out_s; - } -}; -} // namespace map_based_prediction - -#endif // CUBIC_SPLINE_HPP_ diff --git a/perception/map_based_prediction/include/map_based_prediction.hpp b/perception/map_based_prediction/include/map_based_prediction.hpp deleted file mode 100644 index 19678f8c2a482..0000000000000 --- a/perception/map_based_prediction/include/map_based_prediction.hpp +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2018-2019 Autoware Foundation -// -// 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 MAP_BASED_PREDICTION_HPP_ -#define MAP_BASED_PREDICTION_HPP_ - -#include -#include -#include -#include - -#include - -namespace map_based_prediction -{ -using autoware_auto_perception_msgs::msg::ObjectClassification; -using autoware_auto_perception_msgs::msg::PredictedObject; -using autoware_auto_perception_msgs::msg::PredictedObjectKinematics; -using autoware_auto_perception_msgs::msg::PredictedObjects; -using autoware_auto_perception_msgs::msg::PredictedPath; -using autoware_auto_perception_msgs::msg::TrackedObject; -using autoware_auto_perception_msgs::msg::TrackedObjectKinematics; -using autoware_auto_perception_msgs::msg::TrackedObjects; - -struct DynamicObjectWithLanes -{ - TrackedObject object; - std::vector> lanes; - std::vector confidence; -}; - -struct DynamicObjectWithLanesArray -{ - std_msgs::msg::Header header; - std::vector objects; -}; - -class Spline2D; - -class MapBasedPrediction -{ -private: - double interpolating_resolution_; - double time_horizon_; - double sampling_delta_time_; - - bool getPredictedPath( - const double height, const double current_d_position, const double current_d_velocity, - const double current_s_position, const double current_s_velocity, - const std_msgs::msg::Header & origin_header, Spline2D & spline2d, PredictedPath & path) const; - - void getLinearPredictedPath( - const geometry_msgs::msg::Pose & object_pose, const geometry_msgs::msg::Twist & object_twist, - PredictedPath & predicted_path) const; - - void normalizeLikelihood(PredictedObjectKinematics & predicted_object_kinematics); - - PredictedObjectKinematics convertToPredictedKinematics( - const TrackedObjectKinematics & tracked_object); - -public: - MapBasedPrediction( - double interpolating_resolution, double time_horizon, double sampling_delta_time); - - bool doPrediction( - const DynamicObjectWithLanesArray & in_objects, std::vector & out_objects); - - bool doLinearPrediction( - const PredictedObjects & in_objects, std::vector & out_objects); - - PredictedObject convertToPredictedObject(const TrackedObject & tracked_object); -}; -} // namespace map_based_prediction - -#endif // MAP_BASED_PREDICTION_HPP_ diff --git a/perception/map_based_prediction/include/map_based_prediction/map_based_prediction_node.hpp b/perception/map_based_prediction/include/map_based_prediction/map_based_prediction_node.hpp new file mode 100644 index 0000000000000..57f2fe3f2120c --- /dev/null +++ b/perception/map_based_prediction/include/map_based_prediction/map_based_prediction_node.hpp @@ -0,0 +1,200 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 MAP_BASED_PREDICTION__MAP_BASED_PREDICTION_NODE_HPP_ +#define MAP_BASED_PREDICTION__MAP_BASED_PREDICTION_NODE_HPP_ + +#include "map_based_prediction/path_generator.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace map_based_prediction +{ + +struct ObjectData +{ + std_msgs::msg::Header header; + lanelet::ConstLanelets current_lanelets; + lanelet::ConstLanelets future_possible_lanelets; + geometry_msgs::msg::Pose pose; + geometry_msgs::msg::Twist twist; + double time_delay; +}; + +enum class Maneuver { + LANE_FOLLOW = 0, + LEFT_LANE_CHANGE = 1, + RIGHT_LANE_CHANGE = 2, +}; + +struct LaneletData +{ + lanelet::Lanelet lanelet; + float probability; +}; + +struct PredictedRefPath +{ + float probability; + PosePath path; + Maneuver maneuver; +}; + +using LaneletsData = std::vector; +using ManeuverProbability = std::unordered_map; +using autoware_auto_mapping_msgs::msg::HADMapBin; +using autoware_auto_perception_msgs::msg::ObjectClassification; +using autoware_auto_perception_msgs::msg::PredictedObject; +using autoware_auto_perception_msgs::msg::PredictedObjectKinematics; +using autoware_auto_perception_msgs::msg::PredictedObjects; +using autoware_auto_perception_msgs::msg::PredictedPath; +using autoware_auto_perception_msgs::msg::TrackedObject; +using autoware_auto_perception_msgs::msg::TrackedObjectKinematics; +using autoware_auto_perception_msgs::msg::TrackedObjects; + +class MapBasedPredictionNode : public rclcpp::Node +{ +public: + explicit MapBasedPredictionNode(const rclcpp::NodeOptions & node_options); + +private: + // ROS Publisher and Subscriber + rclcpp::Publisher::SharedPtr pub_objects_; + rclcpp::Publisher::SharedPtr pub_debug_markers_; + rclcpp::Subscription::SharedPtr sub_objects_; + rclcpp::Subscription::SharedPtr sub_map_; + + // Object History + std::unordered_map> objects_history_; + + // Lanelet Map Pointers + std::shared_ptr lanelet_map_ptr_; + std::shared_ptr routing_graph_ptr_; + std::shared_ptr traffic_rules_ptr_; + + // Pose Transform Listener + tier4_autoware_utils::TransformListener transform_listener_{this}; + + // Path Generator + std::shared_ptr path_generator_; + + // Parameters + bool enable_delay_compensation_; + double prediction_time_horizon_; + double prediction_sampling_time_interval_; + double min_velocity_for_map_based_prediction_; + double debug_accumulated_time_; + double dist_threshold_for_searching_lanelet_; + double delta_yaw_threshold_for_searching_lanelet_; + double sigma_lateral_offset_; + double sigma_yaw_angle_deg_; + double object_buffer_time_length_; + double history_time_length_; + double dist_ratio_threshold_to_left_bound_; + double dist_ratio_threshold_to_right_bound_; + double diff_dist_threshold_to_left_bound_; + double diff_dist_threshold_to_right_bound_; + double reference_path_resolution_; + + // Member Functions + void mapCallback(const HADMapBin::ConstSharedPtr msg); + void objectsCallback(const TrackedObjects::ConstSharedPtr in_objects); + + PredictedObjectKinematics convertToPredictedKinematics( + const TrackedObjectKinematics & tracked_object); + + PredictedObject convertToPredictedObject(const TrackedObject & tracked_object); + + void removeOldObjectsHistory(const double current_time); + + LaneletsData getCurrentLanelets(const TrackedObject & object); + bool checkCloseLaneletCondition( + const std::pair & lanelet, const TrackedObject & object, + const lanelet::BasicPoint2d & search_point); + float calculateLocalLikelihood( + const lanelet::Lanelet & current_lanelet, const TrackedObject & object) const; + static double getObjectYaw(const TrackedObject & object); + + void updateObjectsHistory( + const std_msgs::msg::Header & header, const TrackedObject & object, + const LaneletsData & current_lanelets_data); + + std::vector getPredictedReferencePath( + const TrackedObject & object, const LaneletsData & current_lanelets_data, + const double object_detected_time); + Maneuver predictObjectManeuver( + const TrackedObject & object, const LaneletData & current_lanelet, + const double object_detected_time); + void addValidPath( + const lanelet::routing::LaneletPaths & candidate_paths, + lanelet::routing::LaneletPaths & valid_paths); + geometry_msgs::msg::Pose compensateTimeDelay( + const geometry_msgs::msg::Pose & delayed_pose, const geometry_msgs::msg::Twist & twist, + const double dt) const; + double calcRightLateralOffset( + const lanelet::ConstLineString2d & boundary_line, const geometry_msgs::msg::Pose & search_pose); + double calcLeftLateralOffset( + const lanelet::ConstLineString2d & boundary_line, const geometry_msgs::msg::Pose & search_pose); + ManeuverProbability calculateManeuverProbability( + const Maneuver & predicted_maneuver, const lanelet::routing::LaneletPaths & left_paths, + const lanelet::routing::LaneletPaths & right_paths, + const lanelet::routing::LaneletPaths & center_paths); + + void addReferencePaths( + const TrackedObject & object, const lanelet::routing::LaneletPaths & candidate_paths, + const float path_probability, const ManeuverProbability & maneuver_probability, + const Maneuver & maneuver, std::vector & reference_paths); + std::vector convertPathType(const lanelet::routing::LaneletPaths & paths); + PosePath resamplePath(const PosePath & base_path) const; + + void updateFuturePossibleLanelets( + const TrackedObject & object, const lanelet::routing::LaneletPaths & paths); + + bool isDuplicated( + const std::pair & target_lanelet, + const LaneletsData & lanelets_data); + bool isDuplicated( + const PredictedPath & predicted_path, const std::vector & predicted_paths); + + visualization_msgs::msg::Marker getDebugMarker( + const TrackedObject & object, const Maneuver & maneuver, const size_t obj_num); +}; +} // namespace map_based_prediction + +#endif // MAP_BASED_PREDICTION__MAP_BASED_PREDICTION_NODE_HPP_ diff --git a/perception/map_based_prediction/include/map_based_prediction/path_generator.hpp b/perception/map_based_prediction/include/map_based_prediction/path_generator.hpp new file mode 100644 index 0000000000000..14922078095fb --- /dev/null +++ b/perception/map_based_prediction/include/map_based_prediction/path_generator.hpp @@ -0,0 +1,95 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 MAP_BASED_PREDICTION__PATH_GENERATOR_HPP_ +#define MAP_BASED_PREDICTION__PATH_GENERATOR_HPP_ + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace map_based_prediction +{ +using autoware_auto_perception_msgs::msg::ObjectClassification; +using autoware_auto_perception_msgs::msg::PredictedObject; +using autoware_auto_perception_msgs::msg::PredictedObjectKinematics; +using autoware_auto_perception_msgs::msg::PredictedObjects; +using autoware_auto_perception_msgs::msg::PredictedPath; +using autoware_auto_perception_msgs::msg::TrackedObject; +using autoware_auto_perception_msgs::msg::TrackedObjectKinematics; +using autoware_auto_perception_msgs::msg::TrackedObjects; + +struct FrenetPoint +{ + double s; + double d; + float s_vel; + float d_vel; + float s_acc; + float d_acc; +}; + +using FrenetPath = std::vector; +using PosePath = std::vector; + +class PathGenerator +{ +public: + PathGenerator(const double time_horizon, const double sampling_time_interval); + + PredictedPath generatePathForNonVehicleObject(const TrackedObject & object); + + PredictedPath generatePathForLowSpeedVehicle(const TrackedObject & object) const; + + PredictedPath generatePathForOffLaneVehicle(const TrackedObject & object); + + PredictedPath generatePathForOnLaneVehicle( + const TrackedObject & object, const PosePath & ref_paths); + +private: + // Parameters + double time_horizon_; + double sampling_time_interval_; + + // Member functions + PredictedPath generateStraightPath(const TrackedObject & object) const; + + PredictedPath generatePolynomialPath(const TrackedObject & object, const PosePath & ref_path); + + FrenetPath generateFrenetPath( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double max_length); + Eigen::Vector3d calcLatCoefficients( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double T); + Eigen::Vector2d calcLonCoefficients( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double T); + + PosePath interpolateReferencePath( + const PosePath & base_path, const FrenetPath & frenet_predicted_path); + + PredictedPath convertToPredictedPath( + const TrackedObject & object, const FrenetPath & frenet_predicted_path, + const PosePath & ref_path); + + FrenetPoint getFrenetPoint(const TrackedObject & object, const PosePath & ref_path); +}; +} // namespace map_based_prediction + +#endif // MAP_BASED_PREDICTION__PATH_GENERATOR_HPP_ diff --git a/perception/map_based_prediction/include/map_based_prediction_ros.hpp b/perception/map_based_prediction/include/map_based_prediction_ros.hpp deleted file mode 100644 index 693a711cfa05e..0000000000000 --- a/perception/map_based_prediction/include/map_based_prediction_ros.hpp +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2018-2019 Autoware Foundation -// -// 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 MAP_BASED_PREDICTION_ROS_HPP_ -#define MAP_BASED_PREDICTION_ROS_HPP_ - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace tf2_ros -{ -class Buffer; -class TransformListener; -} // namespace tf2_ros - -namespace lanelet -{ -class Lanelet; -class LaneletMap; -using LaneletMapPtr = std::shared_ptr; -namespace routing -{ -class RoutingGraph; -} // namespace routing -namespace traffic_rules -{ -class TrafficRules; -} // namespace traffic_rules -} // namespace lanelet - -namespace map_based_prediction -{ -using autoware_auto_mapping_msgs::msg::HADMapBin; -using autoware_auto_perception_msgs::msg::ObjectClassification; -using autoware_auto_perception_msgs::msg::PredictedObject; -using autoware_auto_perception_msgs::msg::PredictedObjects; -using autoware_auto_perception_msgs::msg::TrackedObject; -using autoware_auto_perception_msgs::msg::TrackedObjects; - -struct ObjectData -{ - lanelet::ConstLanelets current_lanelets; - lanelet::ConstLanelets future_possible_lanelets; - geometry_msgs::msg::PoseStamped pose; -}; - -enum class Maneuver { - LANE_FOLLOW, - LEFT_LANE_CHANGE, - RIGHT_LANE_CHANGE, -}; - -class MapBasedPrediction; - -class MapBasedPredictionROS : public rclcpp::Node -{ -private: - double prediction_time_horizon_; - double prediction_sampling_delta_time_; - double min_velocity_for_map_based_prediction_; - double interpolating_resolution_; - double debug_accumulated_time_; - double dist_threshold_for_searching_lanelet_; - double delta_yaw_threshold_for_searching_lanelet_; - double sigma_lateral_offset_; - double sigma_yaw_angle_; - double history_time_length_; - double dist_ratio_threshold_to_left_bound_; - double dist_ratio_threshold_to_right_bound_; - double diff_dist_threshold_to_left_bound_; - double diff_dist_threshold_to_right_bound_; - - rclcpp::Subscription::SharedPtr sub_objects_; - rclcpp::Subscription::SharedPtr sub_map_; - rclcpp::Publisher::SharedPtr pub_objects_; - rclcpp::Publisher::SharedPtr pub_markers_; - - std::unordered_map> object_buffer_; - - std::shared_ptr tf_buffer_ptr_; - std::shared_ptr tf_listener_ptr_; - - std::shared_ptr lanelet_map_ptr_; - std::shared_ptr routing_graph_ptr_; - std::shared_ptr traffic_rules_ptr_; - std::shared_ptr map_based_prediction_; - - bool getSelfPose(geometry_msgs::msg::Pose & self_pose, const std_msgs::msg::Header & header); - bool getSelfPoseInMap(geometry_msgs::msg::Pose & self_pose); - - double getObjectYaw(const TrackedObject & object); - double calculateLikelihood( - const std::vector & path, const TrackedObject & object); - - void addValidPath( - const lanelet::routing::LaneletPaths & candidate_paths, - lanelet::routing::LaneletPaths & valid_paths); - - void objectsCallback(const TrackedObjects::ConstSharedPtr in_objects); - void mapCallback(const HADMapBin::ConstSharedPtr msg); - - bool getClosestLanelets( - const TrackedObject & object, const lanelet::LaneletMapPtr & lanelet_map_ptr, - lanelet::ConstLanelets & closest_lanelets); - - bool checkCloseLaneletCondition( - const std::pair & lanelet, const TrackedObject & object, - const lanelet::BasicPoint2d & search_point); - - void removeInvalidObject(const double current_time); - bool updateObjectBuffer( - const std_msgs::msg::Header & header, const TrackedObject & object, - lanelet::ConstLanelets & current_lanelets); - void updatePossibleLanelets( - const std::string object_id, const lanelet::routing::LaneletPaths & paths); - - double calcRightLateralOffset( - const lanelet::ConstLineString2d & bound_line, const geometry_msgs::msg::Pose & search_pose); - double calcLeftLateralOffset( - const lanelet::ConstLineString2d & bound_line, const geometry_msgs::msg::Pose & search_pose); - Maneuver detectLaneChange( - const TrackedObject & object, const lanelet::ConstLanelet & current_lanelet, - const double current_time); - -public: - explicit MapBasedPredictionROS(const rclcpp::NodeOptions & node_options); -}; -} // namespace map_based_prediction - -#endif // MAP_BASED_PREDICTION_ROS_HPP_ diff --git a/perception/map_based_prediction/launch/map_based_prediction.launch.xml b/perception/map_based_prediction/launch/map_based_prediction.launch.xml index 573fac87e379e..7c917be0fd687 100644 --- a/perception/map_based_prediction/launch/map_based_prediction.launch.xml +++ b/perception/map_based_prediction/launch/map_based_prediction.launch.xml @@ -1,33 +1,39 @@ + - + + + + - + + + diff --git a/perception/map_based_prediction/package.xml b/perception/map_based_prediction/package.xml index 2a2dd0690ee76..58843ee976164 100644 --- a/perception/map_based_prediction/package.xml +++ b/perception/map_based_prediction/package.xml @@ -11,6 +11,7 @@ ament_cmake autoware_auto_perception_msgs + interpolation lanelet2_extension rclcpp rclcpp_components @@ -19,6 +20,7 @@ tf2_ros tier4_autoware_utils unique_identifier_msgs + visualization_msgs ament_lint_auto autoware_lint_common diff --git a/perception/map_based_prediction/src/debug.cpp b/perception/map_based_prediction/src/debug.cpp new file mode 100644 index 0000000000000..f78879ba209a1 --- /dev/null +++ b/perception/map_based_prediction/src/debug.cpp @@ -0,0 +1,49 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 "map_based_prediction/map_based_prediction_node.hpp" +#include "tier4_autoware_utils/ros/marker_helper.hpp" + +namespace map_based_prediction +{ +visualization_msgs::msg::Marker MapBasedPredictionNode::getDebugMarker( + const TrackedObject & object, const Maneuver & maneuver, const size_t obj_num) +{ + visualization_msgs::msg::Marker marker{}; + marker.header.frame_id = "map"; + marker.ns = "maneuver"; + + marker.id = static_cast(obj_num); + marker.lifetime = rclcpp::Duration::from_seconds(1.0); + marker.type = visualization_msgs::msg::Marker::CUBE; + marker.action = visualization_msgs::msg::Marker::ADD; + marker.pose = object.kinematics.pose_with_covariance.pose; + marker.scale = tier4_autoware_utils::createMarkerScale(3.0, 1.0, 1.0); + + // Color by maneuver + double r = 0.0; + double g = 0.0; + double b = 0.0; + if (maneuver == Maneuver::LEFT_LANE_CHANGE) { + g = 1.0; + } else if (maneuver == Maneuver::RIGHT_LANE_CHANGE) { + r = 1.0; + } else { + b = 1.0; + } + marker.color = tier4_autoware_utils::createMarkerColor(r, g, b, 0.8); + + return marker; +} +} // namespace map_based_prediction diff --git a/perception/map_based_prediction/src/map_based_prediction.cpp b/perception/map_based_prediction/src/map_based_prediction.cpp deleted file mode 100644 index 7bd9f7498590d..0000000000000 --- a/perception/map_based_prediction/src/map_based_prediction.cpp +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2020 Tier IV, Inc. -// -// 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 -#include -#include - -#include - -#include -#include -#include - -namespace map_based_prediction -{ -MapBasedPrediction::MapBasedPrediction( - double interpolating_resolution, double time_horizon, double sampling_delta_time) -: interpolating_resolution_(interpolating_resolution), - time_horizon_(time_horizon), - sampling_delta_time_(sampling_delta_time) -{ -} - -bool MapBasedPrediction::doPrediction( - const DynamicObjectWithLanesArray & in_objects, std::vector & out_objects) -{ - for (auto & object_with_lanes : in_objects.objects) { - PredictedObject tmp_object; - tmp_object = convertToPredictedObject(object_with_lanes.object); - for (size_t path_id = 0; path_id < object_with_lanes.lanes.size(); ++path_id) { - std::vector tmp_x; - std::vector tmp_y; - std::vector path = object_with_lanes.lanes.at(path_id); - for (size_t i = 0; i < path.size(); i++) { - if (i > 0) { - double dist = tier4_autoware_utils::calcDistance2d(path.at(i), path.at(i - 1)); - if (dist < interpolating_resolution_) { - continue; - } - } - tmp_x.push_back(path.at(i).position.x); - tmp_y.push_back(path.at(i).position.y); - } - - // Generate Splined Trajectory Arc-Length Position and Yaw angle - Spline2D spline2d(tmp_x, tmp_y); - std::vector interpolated_points; - std::vector interpolated_yaws; - for (double s = 0.0; s < spline2d.s.back(); s += interpolating_resolution_) { - std::array point1 = spline2d.calc_position(s); - geometry_msgs::msg::Point g_point; - g_point.x = point1[0]; - g_point.y = point1[1]; - g_point.z = object_with_lanes.object.kinematics.pose_with_covariance.pose.position.z; - interpolated_points.push_back(g_point); - interpolated_yaws.push_back(spline2d.calc_yaw(s)); - } - - // calculate initial position in Frenet coordinate - // Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame - // Path Planning for Highly Automated Driving on Embedded GPUs - geometry_msgs::msg::Point object_point = - object_with_lanes.object.kinematics.pose_with_covariance.pose.position; - - const size_t nearest_segment_idx = - tier4_autoware_utils::findNearestSegmentIndex(interpolated_points, object_point); - const double l = tier4_autoware_utils::calcLongitudinalOffsetToSegment( - interpolated_points, nearest_segment_idx, object_point); - const double current_s_position = - tier4_autoware_utils::calcSignedArcLength(interpolated_points, 0, nearest_segment_idx) + l; - if (current_s_position > spline2d.s.back()) { - continue; - } - std::array s_point = spline2d.calc_position(current_s_position); - geometry_msgs::msg::Point nearest_point = - tier4_autoware_utils::createPoint(s_point[0], s_point[1], object_point.z); - double current_d_position = tier4_autoware_utils::calcDistance2d(nearest_point, object_point); - const double lane_yaw = spline2d.calc_yaw(current_s_position); - const std::vector origin_v = {std::cos(lane_yaw), std::sin(lane_yaw)}; - const std::vector object_v = { - object_point.x - nearest_point.x, object_point.y - nearest_point.y}; - const double cross2d = object_v.at(0) * origin_v.at(1) - object_v.at(1) * origin_v.at(0); - if (cross2d < 0) { - current_d_position *= -1; - } - - // Does not consider orientation of twist since predicting lane-direction - const double current_d_velocity = - object_with_lanes.object.kinematics.twist_with_covariance.twist.linear.y; - const double current_s_velocity = - std::fabs(object_with_lanes.object.kinematics.twist_with_covariance.twist.linear.x); - - // Predict Path - PredictedPath predicted_path; - getPredictedPath( - object_point.z, current_d_position, current_d_velocity, current_s_position, - current_s_velocity, in_objects.header, spline2d, predicted_path); - // substitute confidence value - predicted_path.confidence = object_with_lanes.confidence.at(path_id); - - // Check if the path is already generated - const double CLOSE_PATH_THRESHOLD = 0.1; - bool duplicate_flag = false; - for (const auto & prev_path : tmp_object.kinematics.predicted_paths) { - const auto prev_path_end = prev_path.path.back().position; - const auto current_path_end = predicted_path.path.back().position; - const double dist = tier4_autoware_utils::calcDistance2d(prev_path_end, current_path_end); - if (dist < CLOSE_PATH_THRESHOLD) { - duplicate_flag = true; - break; - } - } - - if (duplicate_flag) { - continue; - } - - tmp_object.kinematics.predicted_paths.push_back(predicted_path); - } - - normalizeLikelihood(tmp_object.kinematics); - out_objects.push_back(tmp_object); - } - return true; -} - -bool MapBasedPrediction::doLinearPrediction( - const PredictedObjects & in_objects, std::vector & out_objects) -{ - for (const auto & object : in_objects.objects) { - PredictedPath path; - getLinearPredictedPath( - object.kinematics.initial_pose_with_covariance.pose, - object.kinematics.initial_twist_with_covariance.twist, path); - PredictedObject tmp_object; - tmp_object = object; - tmp_object.kinematics.predicted_paths.push_back(path); - out_objects.push_back(tmp_object); - } - - return true; -} - -PredictedObject MapBasedPrediction::convertToPredictedObject(const TrackedObject & tracked_object) -{ - PredictedObject output; - output.object_id = tracked_object.object_id; - output.existence_probability = tracked_object.existence_probability; - output.classification = tracked_object.classification; - output.kinematics = convertToPredictedKinematics(tracked_object.kinematics); - output.shape = tracked_object.shape; - return output; -} - -PredictedObjectKinematics MapBasedPrediction::convertToPredictedKinematics( - const TrackedObjectKinematics & tracked_object) -{ - PredictedObjectKinematics output; - output.initial_pose_with_covariance = tracked_object.pose_with_covariance; - output.initial_twist_with_covariance = tracked_object.twist_with_covariance; - output.initial_acceleration_with_covariance = tracked_object.acceleration_with_covariance; - return output; -} - -void MapBasedPrediction::normalizeLikelihood( - PredictedObjectKinematics & predicted_object_kinematics) -{ - // might not be the smartest way - double sum_confidence = 0.0; - for (const auto & path : predicted_object_kinematics.predicted_paths) { - sum_confidence += path.confidence; - } - - for (auto & path : predicted_object_kinematics.predicted_paths) { - path.confidence = path.confidence / sum_confidence; - } -} - -bool MapBasedPrediction::getPredictedPath( - const double height, const double current_d_position, const double current_d_velocity, - const double current_s_position, const double current_s_velocity, - [[maybe_unused]] const std_msgs::msg::Header & origin_header, Spline2D & spline2d, - PredictedPath & path) const -{ - // Quintic polynomial for d - // A = np.array([[T**3, T**4, T**5], - // [3 * T ** 2, 4 * T ** 3, 5 * T ** 4], - // [6 * T, 12 * T ** 2, 20 * T ** 3]]) - // A_inv = np.matrix([[10/(T**3), -4/(T**2), 1/(2*T)], - // [-15/(T**4), 7/(T**3), -1/(T**2)], - // [6/(T**5), -3/(T**4), 1/(2*T**3)]]) - // b = np.matrix([[xe - self.a0 - self.a1 * T - self.a2 * T**2], - // [vxe - self.a1 - 2 * self.a2 * T], - // [axe - 2 * self.a2]]) - double target_d_position = 0; - - double t = time_horizon_; - Eigen::Matrix3d a_3_inv; - a_3_inv << 10 / std::pow(t, 3), -4 / std::pow(t, 2), 1 / (2 * t), -15 / std::pow(t, 4), - 7 / std::pow(t, 3), -1 / std::pow(t, 2), 6 / std::pow(t, 5), -3 / std::pow(t, 4), - 1 / (2 * std::pow(t, 3)); - - double target_d_velocity = current_d_velocity; - double target_d_acceleration = 0; - Eigen::Vector3d b_3; - b_3 << target_d_position - current_d_position - current_d_velocity * t, - target_d_velocity - current_d_velocity, target_d_acceleration; - - Eigen::Vector3d x_3; - x_3 = a_3_inv * b_3; - - // Quadric polynomial - // A_inv = np.matrix([[1/(T**2), -1/(3*T)], - // [-1/(2*T**3), 1/(4*T**2)]]) - // b = np.matrix([[vxe - self.a1 - 2 * self.a2 * T], - // [axe - 2 * self.a2]]) - Eigen::Matrix2d a_2_inv; - a_2_inv << 1 / std::pow(t, 2), -1 / (3 * t), -1 / (2 * std::pow(t, 3)), 1 / (4 * std::pow(t, 2)); - double target_s_velocity = current_s_velocity; - Eigen::Vector2d b_2; - b_2 << target_s_velocity - current_s_velocity, 0; - Eigen::Vector2d x_2; - x_2 = a_2_inv * b_2; - - // sampling points from calculated path - double dt = sampling_delta_time_; - std::vector d_vec; - for (double i = 0; i < t; i += dt) { - const double calculated_d = current_d_position + current_d_velocity * i + 0 * 2 * i * i + - x_3(0) * i * i * i + x_3(1) * i * i * i * i + - x_3(2) * i * i * i * i * i; - const double calculated_s = current_s_position + current_s_velocity * i + 2 * 0 * i * i + - x_2(0) * i * i * i + x_2(1) * i * i * i * i; - - geometry_msgs::msg::Pose tmp_point; - if (calculated_s > spline2d.s.back()) { - break; - } - std::array p = spline2d.calc_position(calculated_s); - double yaw = spline2d.calc_yaw(calculated_s); - tmp_point.position.x = p[0] + std::cos(yaw - M_PI / 2.0) * calculated_d; - tmp_point.position.y = p[1] + std::sin(yaw - M_PI / 2.0) * calculated_d; - tmp_point.position.z = height; - tf2::Quaternion quat; - quat.setRPY(0.0, 0.0, yaw); - tmp_point.orientation = tf2::toMsg(quat); - path.path.push_back(tmp_point); - if (path.path.size() >= path.path.max_size()) { - break; - } - } - - path.time_step = rclcpp::Duration::from_seconds(dt); - return true; -} - -void MapBasedPrediction::getLinearPredictedPath( - const geometry_msgs::msg::Pose & object_pose, const geometry_msgs::msg::Twist & object_twist, - PredictedPath & predicted_path) const -{ - const double & sampling_delta_time = sampling_delta_time_; - const double & time_horizon = time_horizon_; - const double ep = 0.001; - - for (double dt = 0.0; dt < time_horizon + ep; dt += sampling_delta_time) { - geometry_msgs::msg::Pose tmp_pose; - geometry_msgs::msg::Pose object_frame_pose; - geometry_msgs::msg::Pose world_frame_pose; - object_frame_pose.position.x = object_twist.linear.x * dt; - object_frame_pose.position.y = object_twist.linear.y * dt; - tf2::Quaternion quat; - quat.setRPY(0.0, 0.0, 0.0); - object_frame_pose.orientation = tf2::toMsg(quat); - tf2::Transform tf_object2future; - tf2::Transform tf_world2object; - tf2::Transform tf_world2future; - - tf2::fromMsg(object_pose, tf_world2object); - tf2::fromMsg(object_frame_pose, tf_object2future); - tf_world2future = tf_world2object * tf_object2future; - tf2::toMsg(tf_world2future, world_frame_pose); - tmp_pose = world_frame_pose; - predicted_path.path.push_back(tmp_pose); - if (predicted_path.path.size() >= predicted_path.path.max_size()) { - break; - } - } - - predicted_path.confidence = 1.0; - predicted_path.time_step = rclcpp::Duration::from_seconds(sampling_delta_time); -} -} // namespace map_based_prediction diff --git a/perception/map_based_prediction/src/map_based_prediction_node.cpp b/perception/map_based_prediction/src/map_based_prediction_node.cpp new file mode 100644 index 0000000000000..39722cc0eddf7 --- /dev/null +++ b/perception/map_based_prediction/src/map_based_prediction_node.cpp @@ -0,0 +1,999 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 "map_based_prediction/map_based_prediction_node.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ +std::string toHexString(const unique_identifier_msgs::msg::UUID & id) +{ + std::stringstream ss; + for (auto i = 0; i < 16; ++i) { + ss << std::hex << std::setfill('0') << std::setw(2) << +id.uuid[i]; + } + return ss.str(); +} + +lanelet::ConstLanelets getLanelets(const map_based_prediction::LaneletsData & data) +{ + lanelet::ConstLanelets lanelets; + for (const auto & lanelet_data : data) { + lanelets.push_back(lanelet_data.lanelet); + } + + return lanelets; +} +} // namespace + +namespace map_based_prediction +{ +MapBasedPredictionNode::MapBasedPredictionNode(const rclcpp::NodeOptions & node_options) +: Node("map_based_prediction", node_options), debug_accumulated_time_(0.0) +{ + enable_delay_compensation_ = declare_parameter("enable_delay_compensation", true); + prediction_time_horizon_ = declare_parameter("prediction_time_horizon", 10.0); + prediction_sampling_time_interval_ = declare_parameter("prediction_sampling_delta_time", 0.5); + min_velocity_for_map_based_prediction_ = + declare_parameter("min_velocity_for_map_based_prediction", 1.0); + dist_threshold_for_searching_lanelet_ = + declare_parameter("dist_threshold_for_searching_lanelet", 3.0); + delta_yaw_threshold_for_searching_lanelet_ = + declare_parameter("delta_yaw_threshold_for_searching_lanelet", 0.785); + sigma_lateral_offset_ = declare_parameter("sigma_lateral_offset", 0.5); + sigma_yaw_angle_deg_ = declare_parameter("sigma_yaw_angle_deg", 5.0); + object_buffer_time_length_ = declare_parameter("object_buffer_time_length", 2.0); + history_time_length_ = declare_parameter("history_time_length", 1.0); + dist_ratio_threshold_to_left_bound_ = + declare_parameter("dist_ratio_threshold_to_left_bound", -0.5); + dist_ratio_threshold_to_right_bound_ = + declare_parameter("dist_ratio_threshold_to_right_bound", 0.5); + diff_dist_threshold_to_left_bound_ = declare_parameter("diff_dist_threshold_to_left_bound", 0.29); + diff_dist_threshold_to_right_bound_ = + declare_parameter("diff_dist_threshold_to_right_bound", -0.29); + reference_path_resolution_ = declare_parameter("reference_path_resolution", 0.5); + + path_generator_ = + std::make_shared(prediction_time_horizon_, prediction_sampling_time_interval_); + + sub_objects_ = this->create_subscription( + "/perception/object_recognition/tracking/objects", 1, + std::bind(&MapBasedPredictionNode::objectsCallback, this, std::placeholders::_1)); + sub_map_ = this->create_subscription( + "/vector_map", rclcpp::QoS{1}.transient_local(), + std::bind(&MapBasedPredictionNode::mapCallback, this, std::placeholders::_1)); + + pub_objects_ = this->create_publisher("objects", rclcpp::QoS{1}); + pub_debug_markers_ = + this->create_publisher("maneuver", rclcpp::QoS{1}); +} + +PredictedObjectKinematics MapBasedPredictionNode::convertToPredictedKinematics( + const TrackedObjectKinematics & tracked_object) +{ + PredictedObjectKinematics output; + output.initial_pose_with_covariance = tracked_object.pose_with_covariance; + output.initial_twist_with_covariance = tracked_object.twist_with_covariance; + output.initial_acceleration_with_covariance = tracked_object.acceleration_with_covariance; + return output; +} + +PredictedObject MapBasedPredictionNode::convertToPredictedObject( + const TrackedObject & tracked_object) +{ + PredictedObject predicted_object; + predicted_object.kinematics = convertToPredictedKinematics(tracked_object.kinematics); + predicted_object.classification = tracked_object.classification; + predicted_object.object_id = tracked_object.object_id; + predicted_object.shape = tracked_object.shape; + predicted_object.existence_probability = tracked_object.existence_probability; + + return predicted_object; +} + +void MapBasedPredictionNode::mapCallback(const HADMapBin::ConstSharedPtr msg) +{ + RCLCPP_INFO(get_logger(), "[Map Based Prediction]: Start loading lanelet"); + lanelet_map_ptr_ = std::make_shared(); + lanelet::utils::conversion::fromBinMsg( + *msg, lanelet_map_ptr_, &traffic_rules_ptr_, &routing_graph_ptr_); + RCLCPP_INFO(get_logger(), "[Map Based Prediction]: Map is loaded"); +} + +void MapBasedPredictionNode::objectsCallback(const TrackedObjects::ConstSharedPtr in_objects) +{ + // Guard for map pointer and frame transformation + if (!lanelet_map_ptr_) { + return; + } + + auto world2map_transform = transform_listener_.getTransform( + "map", // target + in_objects->header.frame_id, // src + in_objects->header.stamp, rclcpp::Duration::from_seconds(0.1)); + auto map2world_transform = transform_listener_.getTransform( + in_objects->header.frame_id, // target + "map", // src + in_objects->header.stamp, rclcpp::Duration::from_seconds(0.1)); + auto debug_map2lidar_transform = transform_listener_.getTransform( + "base_link", // target + "map", // src + rclcpp::Time(), rclcpp::Duration::from_seconds(0.1)); + + if (!world2map_transform || !map2world_transform || !debug_map2lidar_transform) { + return; + } + + // Remove old objects information in object history + const double objects_detected_time = rclcpp::Time(in_objects->header.stamp).seconds(); + removeOldObjectsHistory(objects_detected_time); + + // result output + PredictedObjects output; + output.header = in_objects->header; + output.header.frame_id = "map"; + + // result debug + visualization_msgs::msg::MarkerArray debug_markers; + + for (const auto & object : in_objects->objects) { + std::string object_id = toHexString(object.object_id); + TrackedObject transformed_object = object; + + // transform object frame if it's based on map frame + if (in_objects->header.frame_id != "map") { + geometry_msgs::msg::PoseStamped pose_in_map; + geometry_msgs::msg::PoseStamped pose_orig; + pose_orig.pose = object.kinematics.pose_with_covariance.pose; + tf2::doTransform(pose_orig, pose_in_map, *world2map_transform); + transformed_object.kinematics.pose_with_covariance.pose = pose_in_map.pose; + } + + // For non-vehicle object + if ( + transformed_object.classification.front().label != ObjectClassification::CAR && + transformed_object.classification.front().label != ObjectClassification::BUS && + transformed_object.classification.front().label != ObjectClassification::TRAILER && + transformed_object.classification.front().label != ObjectClassification::MOTORCYCLE && + transformed_object.classification.front().label != ObjectClassification::TRUCK) { + PredictedPath predicted_path = + path_generator_->generatePathForNonVehicleObject(transformed_object); + predicted_path.confidence = 1.0; + + auto predicted_object = convertToPredictedObject(transformed_object); + predicted_object.kinematics.predicted_paths.push_back(predicted_path); + output.objects.push_back(predicted_object); + continue; + } + + // Get Closest Lanelet + const auto current_lanelets = getCurrentLanelets(transformed_object); + + // Update Objects History + updateObjectsHistory(output.header, transformed_object, current_lanelets); + + // For off lane obstacles + if (current_lanelets.empty()) { + PredictedPath predicted_path = + path_generator_->generatePathForOffLaneVehicle(transformed_object); + predicted_path.confidence = 1.0; + if (predicted_path.path.empty()) { + continue; + } + + auto predicted_object = convertToPredictedObject(transformed_object); + predicted_object.kinematics.predicted_paths.push_back(predicted_path); + output.objects.push_back(predicted_object); + continue; + } + + // For too-slow vehicle + if ( + std::fabs(transformed_object.kinematics.twist_with_covariance.twist.linear.x) < + min_velocity_for_map_based_prediction_) { + PredictedPath predicted_path = + path_generator_->generatePathForLowSpeedVehicle(transformed_object); + predicted_path.confidence = 1.0; + if (predicted_path.path.empty()) { + continue; + } + + auto predicted_object = convertToPredictedObject(transformed_object); + predicted_object.kinematics.predicted_paths.push_back(predicted_path); + output.objects.push_back(predicted_object); + continue; + } + + // Get Predicted Reference Path for Each Maneuver and current lanelets + // return: + const auto ref_paths = + getPredictedReferencePath(transformed_object, current_lanelets, objects_detected_time); + + // If predicted reference path is empty, assume this object is out of the lane + if (ref_paths.empty()) { + PredictedPath predicted_path = + path_generator_->generatePathForLowSpeedVehicle(transformed_object); + predicted_path.confidence = 1.0; + if (predicted_path.path.empty()) { + continue; + } + + auto predicted_object = convertToPredictedObject(transformed_object); + predicted_object.kinematics.predicted_paths.push_back(predicted_path); + output.objects.push_back(predicted_object); + continue; + } + + // Get Debug Marker for On Lane Vehicles + const auto max_prob_path = std::max_element( + ref_paths.begin(), ref_paths.end(), + [](const PredictedRefPath & a, const PredictedRefPath & b) { + return a.probability < b.probability; + }); + const auto debug_marker = + getDebugMarker(object, max_prob_path->maneuver, debug_markers.markers.size()); + debug_markers.markers.push_back(debug_marker); + + // Generate Predicted Path + std::vector predicted_paths; + for (const auto & ref_path : ref_paths) { + PredictedPath predicted_path = + path_generator_->generatePathForOnLaneVehicle(transformed_object, ref_path.path); + predicted_path.confidence = ref_path.probability; + + predicted_paths.push_back(predicted_path); + } + + // Normalize Path Confidence and output the predicted object + { + float sum_confidence = 0.0; + for (const auto & predicted_path : predicted_paths) { + sum_confidence += predicted_path.confidence; + } + const float min_sum_confidence_value = 1e-3; + sum_confidence = std::max(sum_confidence, min_sum_confidence_value); + + for (auto & predicted_path : predicted_paths) { + predicted_path.confidence = predicted_path.confidence / sum_confidence; + } + + auto predicted_object = convertToPredictedObject(transformed_object); + for (const auto & predicted_path : predicted_paths) { + predicted_object.kinematics.predicted_paths.push_back(predicted_path); + } + output.objects.push_back(predicted_object); + } + } + + // Publish Results + pub_objects_->publish(output); + pub_debug_markers_->publish(debug_markers); +} + +double MapBasedPredictionNode::getObjectYaw(const TrackedObject & object) +{ + if (object.kinematics.orientation_availability) { + return tf2::getYaw(object.kinematics.pose_with_covariance.pose.orientation); + } + + const auto & object_pose = object.kinematics.pose_with_covariance.pose; + const auto & object_twist = object.kinematics.twist_with_covariance.twist; + const auto future_object_pose = tier4_autoware_utils::calcOffsetPose( + object_pose, object_twist.linear.x * 0.1, object_twist.linear.y * 0.1, 0.0); + return tier4_autoware_utils::calcAzimuthAngle(future_object_pose.position, object_pose.position); +} + +void MapBasedPredictionNode::removeOldObjectsHistory(const double current_time) +{ + std::vector invalid_object_id; + for (auto iter = objects_history_.begin(); iter != objects_history_.end(); ++iter) { + const std::string object_id = iter->first; + std::deque & object_data = iter->second; + + // If object data is empty, we are going to delete the buffer for the obstacle + if (object_data.empty()) { + invalid_object_id.push_back(object_id); + continue; + } + + const double latest_object_time = rclcpp::Time(object_data.back().header.stamp).seconds(); + + // Delete Old Objects + if (current_time - latest_object_time > 2.0) { + invalid_object_id.push_back(object_id); + continue; + } + + // Delete old information + while (!object_data.empty()) { + const double post_object_time = rclcpp::Time(object_data.front().header.stamp).seconds(); + if (current_time - post_object_time > 2.0) { + // Delete Old Position + object_data.pop_front(); + } else { + break; + } + } + + if (object_data.empty()) { + invalid_object_id.push_back(object_id); + continue; + } + } + + for (const auto & key : invalid_object_id) { + objects_history_.erase(key); + } +} + +LaneletsData MapBasedPredictionNode::getCurrentLanelets(const TrackedObject & object) +{ + // obstacle point + lanelet::BasicPoint2d search_point( + object.kinematics.pose_with_covariance.pose.position.x, + object.kinematics.pose_with_covariance.pose.position.y); + + // nearest lanelet + std::vector> surrounding_lanelets = + lanelet::geometry::findNearest(lanelet_map_ptr_->laneletLayer, search_point, 10); + + // No Closest Lanelets + if (surrounding_lanelets.empty()) { + return {}; + } + + LaneletsData closest_lanelets; + for (const auto & lanelet : surrounding_lanelets) { + // Check if the close lanelets meet the necessary condition for start lanelets and + // Check if similar lanelet is inside the closest lanelet + if ( + !checkCloseLaneletCondition(lanelet, object, search_point) || + isDuplicated(lanelet, closest_lanelets)) { + continue; + } + + LaneletData closest_lanelet; + closest_lanelet.lanelet = lanelet.second; + closest_lanelet.probability = calculateLocalLikelihood(lanelet.second, object); + closest_lanelets.push_back(closest_lanelet); + } + + return closest_lanelets; +} + +bool MapBasedPredictionNode::checkCloseLaneletCondition( + const std::pair & lanelet, const TrackedObject & object, + const lanelet::BasicPoint2d & search_point) +{ + // Step1. If we only have one point in the centerline, we will ignore the lanelet + if (lanelet.second.centerline().size() <= 1) { + return false; + } + + // Step2. Check if the obstacle is inside of this lanelet + if (!lanelet::geometry::inside(lanelet.second, search_point)) { + return false; + } + + // If the object is in the objects history, we check if the target lanelet is + // inside the current lanelets id or following lanelets + const std::string object_id = toHexString(object.object_id); + if (objects_history_.count(object_id) != 0) { + const std::vector & possible_lanelet = + objects_history_.at(object_id).back().future_possible_lanelets; + + bool not_in_possible_lanelet = + std::find(possible_lanelet.begin(), possible_lanelet.end(), lanelet.second) == + possible_lanelet.end(); + if (!possible_lanelet.empty() && not_in_possible_lanelet) { + return false; + } + } + + // Step3. Calculate the angle difference between the lane angle and obstacle angle + double object_yaw = getObjectYaw(object); + const double lane_yaw = lanelet::utils::getLaneletAngle( + lanelet.second, object.kinematics.pose_with_covariance.pose.position); + const double delta_yaw = object_yaw - lane_yaw; + const double normalized_delta_yaw = std::atan2(std::sin(delta_yaw), std::cos(delta_yaw)); + const double abs_norm_delta = std::fabs(normalized_delta_yaw); + + // Step4. Check if the closest lanelet is valid, and add all + // of the lanelets that are below max_dist and max_delta_yaw + if ( + lanelet.first < dist_threshold_for_searching_lanelet_ && + abs_norm_delta < delta_yaw_threshold_for_searching_lanelet_) { + return true; + } + + return false; +} + +float MapBasedPredictionNode::calculateLocalLikelihood( + const lanelet::Lanelet & current_lanelet, const TrackedObject & object) const +{ + const auto & obj_point = object.kinematics.pose_with_covariance.pose.position; + + // compute yaw difference between the object and lane + const double obj_yaw = getObjectYaw(object); + const double lane_yaw = lanelet::utils::getLaneletAngle(current_lanelet, obj_point); + const double delta_yaw = obj_yaw - lane_yaw; + const double abs_norm_delta_yaw = std::fabs(tier4_autoware_utils::normalizeRadian(delta_yaw)); + + // compute lateral distance + const auto centerline = current_lanelet.centerline(); + std::vector converted_centerline; + for (const auto & p : centerline) { + const auto converted_p = lanelet::utils::conversion::toGeomMsgPt(p); + converted_centerline.push_back(converted_p); + } + const double lat_dist = + std::fabs(tier4_autoware_utils::calcLateralOffset(converted_centerline, obj_point)); + + // Compute Chi-squared distributed (Equation (8) in the paper) + const double sigma_d = sigma_lateral_offset_; // Standard Deviation for lateral position + const double sigma_yaw = M_PI * sigma_yaw_angle_deg_ / 180.0; // Standard Deviation for yaw angle + const Eigen::Vector2d delta(lat_dist, abs_norm_delta_yaw); + const Eigen::Matrix2d P_inv = + (Eigen::Matrix2d() << 1.0 / (sigma_d * sigma_d), 0.0, 0.0, 1.0 / (sigma_yaw * sigma_yaw)) + .finished(); + const double MINIMUM_DISTANCE = 1e-6; + const double dist = std::max(delta.dot(P_inv * delta), MINIMUM_DISTANCE); + + return static_cast(1.0 / dist); +} + +void MapBasedPredictionNode::updateObjectsHistory( + const std_msgs::msg::Header & header, const TrackedObject & object, + const LaneletsData & current_lanelets_data) +{ + std::string object_id = toHexString(object.object_id); + const auto current_lanelets = getLanelets(current_lanelets_data); + + ObjectData single_object_data; + single_object_data.header = header; + single_object_data.current_lanelets = current_lanelets; + single_object_data.future_possible_lanelets = current_lanelets; + single_object_data.pose = object.kinematics.pose_with_covariance.pose; + const double object_yaw = getObjectYaw(object); + single_object_data.pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(object_yaw); + single_object_data.time_delay = std::fabs((this->get_clock()->now() - header.stamp).seconds()); + single_object_data.twist = object.kinematics.twist_with_covariance.twist; + + if (objects_history_.count(object_id) == 0) { + // New Object(Create a new object in object histories) + std::deque object_data = {single_object_data}; + objects_history_.emplace(object_id, object_data); + } else { + // Object that is already in the object buffer + std::deque & object_data = objects_history_.at(object_id); + object_data.push_back(single_object_data); + } +} + +std::vector MapBasedPredictionNode::getPredictedReferencePath( + const TrackedObject & object, const LaneletsData & current_lanelets_data, + const double object_detected_time) +{ + const double delta_horizon = 1.0; + const double obj_vel = object.kinematics.twist_with_covariance.twist.linear.x; + + std::vector all_ref_paths; + for (const auto & current_lanelet_data : current_lanelets_data) { + // Step1. Get the path + // Step1.1 Get the left lanelet + lanelet::routing::LaneletPaths left_paths; + auto opt_left = routing_graph_ptr_->left(current_lanelet_data.lanelet); + if (!!opt_left) { + for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { + const double search_dist = horizon * obj_vel + 10.0; + lanelet::routing::LaneletPaths tmp_paths = + routing_graph_ptr_->possiblePaths(*opt_left, search_dist, 0, false); + addValidPath(tmp_paths, left_paths); + } + } + + // Step1.2 Get the right lanelet + lanelet::routing::LaneletPaths right_paths; + auto opt_right = routing_graph_ptr_->right(current_lanelet_data.lanelet); + if (!!opt_right) { + for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { + const double search_dist = horizon * obj_vel + 10.0; + lanelet::routing::LaneletPaths tmp_paths = + routing_graph_ptr_->possiblePaths(*opt_right, search_dist, 0, false); + addValidPath(tmp_paths, right_paths); + } + } + + // Step1.3 Get the centerline + lanelet::routing::LaneletPaths center_paths; + for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { + const double search_dist = horizon * obj_vel + 10.0; + lanelet::routing::LaneletPaths tmp_paths = + routing_graph_ptr_->possiblePaths(current_lanelet_data.lanelet, search_dist, 0, false); + addValidPath(tmp_paths, center_paths); + } + + // Skip calculations if all paths are empty + if (left_paths.empty() && right_paths.empty() && center_paths.empty()) { + continue; + } + + // Step2. Predict Object Maneuver + const Maneuver predicted_maneuver = + predictObjectManeuver(object, current_lanelet_data, object_detected_time); + + // Step3. Allocate probability for each predicted maneuver + const auto maneuver_prob = + calculateManeuverProbability(predicted_maneuver, left_paths, right_paths, center_paths); + + // Step4. add candidate reference paths to the all_ref_paths + const float path_prob = current_lanelet_data.probability; + const auto addReferencePathsLocal = [&](const auto & paths, const auto & maneuver) { + addReferencePaths(object, paths, path_prob, maneuver_prob, maneuver, all_ref_paths); + }; + addReferencePathsLocal(left_paths, Maneuver::LEFT_LANE_CHANGE); + addReferencePathsLocal(right_paths, Maneuver::RIGHT_LANE_CHANGE); + addReferencePathsLocal(center_paths, Maneuver::LANE_FOLLOW); + } + + return all_ref_paths; +} + +void MapBasedPredictionNode::addValidPath( + const lanelet::routing::LaneletPaths & candidate_paths, + lanelet::routing::LaneletPaths & valid_paths) +{ + // Check if candidate paths are already in the valid paths + for (const auto & candidate_path : candidate_paths) { + bool already_searched = false; + for (const auto & valid_path : valid_paths) { + for (const auto & llt : valid_path) { + if (candidate_path.back().id() == llt.id()) { + already_searched = true; + } + } + } + if (!already_searched) { + valid_paths.push_back(candidate_path); + } + } +} + +Maneuver MapBasedPredictionNode::predictObjectManeuver( + const TrackedObject & object, const LaneletData & current_lanelet_data, + const double object_detected_time) +{ + // Step1. Check if we have the object in the buffer + const std::string object_id = toHexString(object.object_id); + if (objects_history_.count(object_id) == 0) { + return Maneuver::LANE_FOLLOW; + } + + const std::deque & object_info = objects_history_.at(object_id); + const double current_time = (this->get_clock()->now()).seconds(); + + // Step2. Get the previous id + int prev_id = static_cast(object_info.size()) - 1; + while (prev_id >= 0) { + const double prev_time_delay = object_info.at(prev_id).time_delay; + const double prev_time = + rclcpp::Time(object_info.at(prev_id).header.stamp).seconds() + prev_time_delay; + // if (object_detected_time - prev_time > history_time_length_) { + if (current_time - prev_time > history_time_length_) { + break; + } + --prev_id; + } + + if (prev_id < 0) { + return Maneuver::LANE_FOLLOW; + } + + // Step3. Get closest previous lanelet ID + const auto & prev_info = object_info.at(static_cast(prev_id)); + const auto prev_pose = compensateTimeDelay(prev_info.pose, prev_info.twist, prev_info.time_delay); + const lanelet::ConstLanelets prev_lanelets = + object_info.at(static_cast(prev_id)).current_lanelets; + if (prev_lanelets.empty()) { + return Maneuver::LANE_FOLLOW; + } + lanelet::ConstLanelet prev_lanelet = prev_lanelets.front(); + double closest_prev_yaw = std::numeric_limits::max(); + for (const auto & lanelet : prev_lanelets) { + const double lane_yaw = lanelet::utils::getLaneletAngle(lanelet, prev_pose.position); + const double delta_yaw = tf2::getYaw(prev_pose.orientation) - lane_yaw; + const double normalized_delta_yaw = tier4_autoware_utils::normalizeRadian(delta_yaw); + if (normalized_delta_yaw < closest_prev_yaw) { + closest_prev_yaw = normalized_delta_yaw; + prev_lanelet = lanelet; + } + } + + // Step4. Check if the vehicle has changed lane + const auto current_lanelet = current_lanelet_data.lanelet; + const double current_time_delay = std::max(current_time - object_detected_time, 0.0); + const auto current_pose = compensateTimeDelay( + object.kinematics.pose_with_covariance.pose, object.kinematics.twist_with_covariance.twist, + current_time_delay); + const double dist = tier4_autoware_utils::calcDistance2d(prev_pose, current_pose); + lanelet::routing::LaneletPaths possible_paths = + routing_graph_ptr_->possiblePaths(prev_lanelet, dist + 2.0, 0, false); + bool has_lane_changed = true; + for (const auto & path : possible_paths) { + for (const auto & lanelet : path) { + if (lanelet == current_lanelet) { + has_lane_changed = false; + break; + } + } + } + + if (has_lane_changed) { + return Maneuver::LANE_FOLLOW; + } + + // Step5. Lane Change Detection + const lanelet::ConstLineString2d prev_left_bound = prev_lanelet.leftBound2d(); + const lanelet::ConstLineString2d prev_right_bound = prev_lanelet.rightBound2d(); + const lanelet::ConstLineString2d current_left_bound = current_lanelet.leftBound2d(); + const lanelet::ConstLineString2d current_right_bound = current_lanelet.rightBound2d(); + const double prev_left_dist = calcLeftLateralOffset(prev_left_bound, prev_pose); + const double prev_right_dist = calcRightLateralOffset(prev_right_bound, prev_pose); + const double current_left_dist = calcLeftLateralOffset(current_left_bound, current_pose); + const double current_right_dist = calcRightLateralOffset(current_right_bound, current_pose); + const double prev_lane_width = std::fabs(prev_left_dist) + std::fabs(prev_right_dist); + const double current_lane_width = std::fabs(current_left_dist) + std::fabs(current_right_dist); + if (prev_lane_width < 1e-3 || current_lane_width < 1e-3) { + RCLCPP_ERROR(get_logger(), "[Map Based Prediction]: Lane Width is too small"); + return Maneuver::LANE_FOLLOW; + } + + const double current_left_dist_ratio = current_left_dist / current_lane_width; + const double current_right_dist_ratio = current_right_dist / current_lane_width; + const double diff_left_current_prev = current_left_dist - prev_left_dist; + const double diff_right_current_prev = current_right_dist - prev_right_dist; + + if ( + current_left_dist_ratio > dist_ratio_threshold_to_left_bound_ && + diff_left_current_prev > diff_dist_threshold_to_left_bound_) { + return Maneuver::LEFT_LANE_CHANGE; + } else if ( + current_right_dist_ratio < dist_ratio_threshold_to_right_bound_ && + diff_right_current_prev < diff_dist_threshold_to_right_bound_) { + return Maneuver::RIGHT_LANE_CHANGE; + } + + return Maneuver::LANE_FOLLOW; +} + +geometry_msgs::msg::Pose MapBasedPredictionNode::compensateTimeDelay( + const geometry_msgs::msg::Pose & delayed_pose, const geometry_msgs::msg::Twist & twist, + const double dt) const +{ + if (!enable_delay_compensation_) { + return delayed_pose; + } + + /* == Nonlinear model == + * + * x_{k+1} = x_k + vx_k * cos(yaw_k) * dt + * y_{k+1} = y_k + vx_k * sin(yaw_k) * dt + * yaw_{k+1} = yaw_k + (wz_k) * dt + * + */ + + const double vx = twist.linear.x; + const double wz = twist.angular.z; + const double prev_yaw = tf2::getYaw(delayed_pose.orientation); + const double prev_x = delayed_pose.position.x; + const double prev_y = delayed_pose.position.y; + const double prev_z = delayed_pose.position.z; + + const double curr_x = prev_x + vx * std::cos(prev_yaw) * dt; + const double curr_y = prev_y + vx * std::sin(prev_yaw) * dt; + const double curr_z = prev_z; + const double curr_yaw = prev_yaw + wz * dt; + + geometry_msgs::msg::Pose current_pose; + current_pose.position = tier4_autoware_utils::createPoint(curr_x, curr_y, curr_z); + current_pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(curr_yaw); + + return current_pose; +} + +double MapBasedPredictionNode::calcRightLateralOffset( + const lanelet::ConstLineString2d & boundary_line, const geometry_msgs::msg::Pose & search_pose) +{ + std::vector boundary_path(boundary_line.size()); + for (size_t i = 0; i < boundary_path.size(); ++i) { + const double x = boundary_line[i].x(); + const double y = boundary_line[i].y(); + boundary_path[i] = tier4_autoware_utils::createPoint(x, y, 0.0); + } + + return std::fabs(tier4_autoware_utils::calcLateralOffset(boundary_path, search_pose.position)); +} + +double MapBasedPredictionNode::calcLeftLateralOffset( + const lanelet::ConstLineString2d & boundary_line, const geometry_msgs::msg::Pose & search_pose) +{ + return -calcRightLateralOffset(boundary_line, search_pose); +} + +void MapBasedPredictionNode::updateFuturePossibleLanelets( + const TrackedObject & object, const lanelet::routing::LaneletPaths & paths) +{ + std::string object_id = toHexString(object.object_id); + if (objects_history_.count(object_id) == 0) { + return; + } + + std::vector & possible_lanelets = + objects_history_.at(object_id).back().future_possible_lanelets; + for (const auto & path : paths) { + for (const auto & lanelet : path) { + bool not_in_buffer = std::find(possible_lanelets.begin(), possible_lanelets.end(), lanelet) == + possible_lanelets.end(); + if (not_in_buffer) { + possible_lanelets.push_back(lanelet); + } + } + } +} + +void MapBasedPredictionNode::addReferencePaths( + const TrackedObject & object, const lanelet::routing::LaneletPaths & candidate_paths, + const float path_probability, const ManeuverProbability & maneuver_probability, + const Maneuver & maneuver, std::vector & reference_paths) +{ + if (!candidate_paths.empty()) { + updateFuturePossibleLanelets(object, candidate_paths); + const auto converted_paths = convertPathType(candidate_paths); + for (const auto & converted_path : converted_paths) { + PredictedRefPath predicted_path; + predicted_path.probability = maneuver_probability.at(maneuver) * path_probability; + predicted_path.path = converted_path; + predicted_path.maneuver = maneuver; + reference_paths.push_back(predicted_path); + } + } +} + +ManeuverProbability MapBasedPredictionNode::calculateManeuverProbability( + const Maneuver & predicted_maneuver, const lanelet::routing::LaneletPaths & left_paths, + const lanelet::routing::LaneletPaths & right_paths, + const lanelet::routing::LaneletPaths & center_paths) +{ + float left_lane_change_probability = 0.0; + float right_lane_change_probability = 0.0; + float lane_follow_probability = 0.0; + if (!left_paths.empty() && predicted_maneuver == Maneuver::LEFT_LANE_CHANGE) { + constexpr float LF_PROB_WHEN_LC = 0.9; // probability for lane follow during lane change + constexpr float LC_PROB_WHEN_LC = 1.0; // probability for left lane change + left_lane_change_probability = LC_PROB_WHEN_LC; + right_lane_change_probability = 0.0; + lane_follow_probability = LF_PROB_WHEN_LC; + } else if (!right_paths.empty() && predicted_maneuver == Maneuver::RIGHT_LANE_CHANGE) { + constexpr float LF_PROB_WHEN_LC = 0.9; // probability for lane follow during lane change + constexpr float RC_PROB_WHEN_LC = 1.0; // probability for right lane change + left_lane_change_probability = 0.0; + right_lane_change_probability = RC_PROB_WHEN_LC; + lane_follow_probability = LF_PROB_WHEN_LC; + } else if (!center_paths.empty()) { + constexpr float LF_PROB = 1.0; // probability for lane follow + constexpr float LC_PROB = 0.3; // probability for left lane change + constexpr float RC_PROB = 0.3; // probability for right lane change + if (predicted_maneuver == Maneuver::LEFT_LANE_CHANGE) { + // If prediction says left change, but left lane is empty, assume lane follow + left_lane_change_probability = 0.0; + right_lane_change_probability = (!right_paths.empty()) ? RC_PROB : 0.0; + } else if (predicted_maneuver == Maneuver::RIGHT_LANE_CHANGE) { + // If prediction says right change, but right lane is empty, assume lane follow + left_lane_change_probability = (!left_paths.empty()) ? LC_PROB : 0.0; + right_lane_change_probability = 0.0; + } else { + // Predicted Maneuver is Lane Follow + left_lane_change_probability = LC_PROB; + right_lane_change_probability = RC_PROB; + } + lane_follow_probability = LF_PROB; + } else { + // Center path is empty + constexpr float LC_PROB = 1.0; // probability for left lane change + constexpr float RC_PROB = 1.0; // probability for right lane change + lane_follow_probability = 0.0; + if (!left_paths.empty() && right_paths.empty()) { + left_lane_change_probability = LC_PROB; + right_lane_change_probability = 0.0; + } else if (left_paths.empty() && !right_paths.empty()) { + left_lane_change_probability = 0.0; + right_lane_change_probability = RC_PROB; + } else { + left_lane_change_probability = LC_PROB; + right_lane_change_probability = RC_PROB; + } + } + + const float MIN_PROBABILITY = 1e-3; + const float max_prob = std::max( + MIN_PROBABILITY, std::max( + lane_follow_probability, + std::max(left_lane_change_probability, right_lane_change_probability))); + + // Insert Normalized Probability + ManeuverProbability maneuver_prob; + maneuver_prob[Maneuver::LEFT_LANE_CHANGE] = left_lane_change_probability / max_prob; + maneuver_prob[Maneuver::RIGHT_LANE_CHANGE] = right_lane_change_probability / max_prob; + maneuver_prob[Maneuver::LANE_FOLLOW] = lane_follow_probability / max_prob; + + return maneuver_prob; +} + +std::vector MapBasedPredictionNode::convertPathType( + const lanelet::routing::LaneletPaths & paths) +{ + std::vector converted_paths; + for (const auto & path : paths) { + PosePath converted_path; + + // Insert Positions. Note that we start inserting points from previous lanelet + if (!path.empty()) { + lanelet::ConstLanelets prev_lanelets = routing_graph_ptr_->previous(path.front()); + if (!prev_lanelets.empty()) { + lanelet::ConstLanelet prev_lanelet = prev_lanelets.front(); + for (const auto & lanelet_p : prev_lanelet.centerline()) { + geometry_msgs::msg::Pose current_p; + current_p.position = + tier4_autoware_utils::createPoint(lanelet_p.x(), lanelet_p.y(), lanelet_p.z()); + const double lane_yaw = lanelet::utils::getLaneletAngle(prev_lanelet, current_p.position); + current_p.orientation = tier4_autoware_utils::createQuaternionFromYaw(lane_yaw); + converted_path.push_back(current_p); + } + } + } + + for (const auto & lanelet : path) { + for (const auto & lanelet_p : lanelet.centerline()) { + geometry_msgs::msg::Pose current_p; + current_p.position = + tier4_autoware_utils::createPoint(lanelet_p.x(), lanelet_p.y(), lanelet_p.z()); + const double lane_yaw = lanelet::utils::getLaneletAngle(lanelet, current_p.position); + current_p.orientation = tier4_autoware_utils::createQuaternionFromYaw(lane_yaw); + + // Prevent from inserting same points + if (!converted_path.empty()) { + const auto prev_p = converted_path.back(); + const double tmp_dist = tier4_autoware_utils::calcDistance2d(prev_p, current_p); + if (tmp_dist < 1e-6) { + continue; + } + } + + converted_path.push_back(current_p); + } + } + + // Resample Path + const auto resampled_converted_path = resamplePath(converted_path); + converted_paths.push_back(resampled_converted_path); + } + + return converted_paths; +} + +PosePath MapBasedPredictionNode::resamplePath(const PosePath & base_path) const +{ + std::vector base_s(base_path.size()); + std::vector base_x(base_path.size()); + std::vector base_y(base_path.size()); + std::vector base_z(base_path.size()); + for (size_t i = 0; i < base_path.size(); ++i) { + base_x.at(i) = base_path.at(i).position.x; + base_y.at(i) = base_path.at(i).position.y; + base_z.at(i) = base_path.at(i).position.z; + base_s.at(i) = tier4_autoware_utils::calcSignedArcLength(base_path, 0, i); + } + const double base_path_len = base_s.back(); + + std::vector resampled_s; + for (double s = 0.0; s <= base_path_len; s += reference_path_resolution_) { + resampled_s.push_back(s); + } + + if (resampled_s.empty()) { + return base_path; + } + + // Insert End Point + const double epsilon = 0.01; + if (std::fabs(resampled_s.back() - base_path_len) < epsilon) { + resampled_s.back() = base_path_len; + } else { + resampled_s.push_back(base_path_len); + } + + if (resampled_s.size() < 2) { + return base_path; + } + + const auto resampled_x = interpolation::lerp(base_s, base_x, resampled_s); + const auto resampled_y = interpolation::lerp(base_s, base_y, resampled_s); + const auto resampled_z = interpolation::lerp(base_s, base_z, resampled_s); + + PosePath resampled_path(resampled_x.size()); + // Position + for (size_t i = 0; i < resampled_path.size(); ++i) { + resampled_path.at(i).position = + tier4_autoware_utils::createPoint(resampled_x.at(i), resampled_y.at(i), resampled_z.at(i)); + } + // Orientation + for (size_t i = 0; i < resampled_path.size() - 1; ++i) { + const auto curr_p = resampled_path.at(i).position; + const auto next_p = resampled_path.at(i + 1).position; + const double yaw = std::atan2(next_p.y - curr_p.y, next_p.x - curr_p.x); + resampled_path.at(i).orientation = tier4_autoware_utils::createQuaternionFromYaw(yaw); + } + resampled_path.back().orientation = resampled_path.at(resampled_path.size() - 2).orientation; + + return resampled_path; +} + +bool MapBasedPredictionNode::isDuplicated( + const std::pair & target_lanelet, + const LaneletsData & lanelets_data) +{ + const double CLOSE_LANELET_THRESHOLD = 0.1; + for (const auto & lanelet_data : lanelets_data) { + const auto target_lanelet_end_p = target_lanelet.second.centerline2d().back(); + const auto lanelet_end_p = lanelet_data.lanelet.centerline2d().back(); + const double dist = std::hypot( + target_lanelet_end_p.x() - lanelet_end_p.x(), target_lanelet_end_p.y() - lanelet_end_p.y()); + if (dist < CLOSE_LANELET_THRESHOLD) { + return true; + } + } + + return false; +} + +bool MapBasedPredictionNode::isDuplicated( + const PredictedPath & predicted_path, const std::vector & predicted_paths) +{ + const double CLOSE_PATH_THRESHOLD = 0.1; + for (const auto & prev_predicted_path : predicted_paths) { + const auto prev_path_end = prev_predicted_path.path.back().position; + const auto current_path_end = predicted_path.path.back().position; + const double dist = tier4_autoware_utils::calcDistance2d(prev_path_end, current_path_end); + if (dist < CLOSE_PATH_THRESHOLD) { + return true; + } + } + + return false; +} +} // namespace map_based_prediction + +#include +RCLCPP_COMPONENTS_REGISTER_NODE(map_based_prediction::MapBasedPredictionNode) diff --git a/perception/map_based_prediction/src/map_based_prediction_ros.cpp b/perception/map_based_prediction/src/map_based_prediction_ros.cpp deleted file mode 100644 index 19df2894e4087..0000000000000 --- a/perception/map_based_prediction/src/map_based_prediction_ros.cpp +++ /dev/null @@ -1,760 +0,0 @@ -// Copyright 2018-2019 Autoware Foundation -// -// 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 "map_based_prediction_ros.hpp" - -#include "map_based_prediction.hpp" - -#include -#include - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -std::string toHexString(const unique_identifier_msgs::msg::UUID & id) -{ - std::stringstream ss; - for (auto i = 0; i < 16; ++i) { - ss << std::hex << std::setfill('0') << std::setw(2) << +id.uuid[i]; - } - return ss.str(); -} -} // namespace -namespace map_based_prediction -{ -MapBasedPredictionROS::MapBasedPredictionROS(const rclcpp::NodeOptions & node_options) -: Node("map_based_prediction", node_options), - interpolating_resolution_(0.5), - debug_accumulated_time_(0.0) -{ - [[maybe_unused]] auto ret = - rcutils_logging_set_logger_level(this->get_logger().get_name(), RCUTILS_LOG_SEVERITY_DEBUG); - - rclcpp::Clock::SharedPtr clock = std::make_shared(RCL_ROS_TIME); - tf_buffer_ptr_ = std::make_shared(clock); - tf_listener_ptr_ = std::make_shared(*tf_buffer_ptr_); - prediction_time_horizon_ = declare_parameter("prediction_time_horizon", 10.0); - prediction_sampling_delta_time_ = declare_parameter("prediction_sampling_delta_time", 0.5); - min_velocity_for_map_based_prediction_ = - declare_parameter("min_velocity_for_map_based_prediction", 1.0); - dist_threshold_for_searching_lanelet_ = - declare_parameter("dist_threshold_for_searching_lanelet", 3.0); - delta_yaw_threshold_for_searching_lanelet_ = - declare_parameter("delta_yaw_threshold_for_searching_lanelet", 0.785); - sigma_lateral_offset_ = declare_parameter("sigma_lateral_offset", 0.5); - sigma_yaw_angle_ = declare_parameter("sigma_yaw_angle", 5.0); - history_time_length_ = declare_parameter("history_time_length", 1.0); - dist_ratio_threshold_to_left_bound_ = - declare_parameter("dist_ratio_threshold_to_left_bound", -0.5); - dist_ratio_threshold_to_right_bound_ = - declare_parameter("dist_ratio_threshold_to_right_bound", 0.5); - diff_dist_threshold_to_left_bound_ = declare_parameter("diff_dist_threshold_to_left_bound", 0.29); - diff_dist_threshold_to_right_bound_ = - declare_parameter("diff_dist_threshold_to_right_bound", -0.29); - - map_based_prediction_ = std::make_shared( - interpolating_resolution_, prediction_time_horizon_, prediction_sampling_delta_time_); - - sub_objects_ = this->create_subscription( - "/perception/object_recognition/tracking/objects", 1, - std::bind(&MapBasedPredictionROS::objectsCallback, this, std::placeholders::_1)); - sub_map_ = this->create_subscription( - "/vector_map", rclcpp::QoS{1}.transient_local(), - std::bind(&MapBasedPredictionROS::mapCallback, this, std::placeholders::_1)); - - pub_objects_ = this->create_publisher("objects", rclcpp::QoS{1}); - pub_markers_ = this->create_publisher( - "objects_path_markers", rclcpp::QoS{1}); -} - -double MapBasedPredictionROS::getObjectYaw(const TrackedObject & object) -{ - if (object.kinematics.orientation_availability) { - return tf2::getYaw(object.kinematics.pose_with_covariance.pose.orientation); - } - - geometry_msgs::msg::Pose object_frame_pose; - geometry_msgs::msg::Pose map_frame_pose; - object_frame_pose.position.x = object.kinematics.twist_with_covariance.twist.linear.x * 0.1; - object_frame_pose.position.y = object.kinematics.twist_with_covariance.twist.linear.y * 0.1; - tf2::Transform tf_object2future; - tf2::Transform tf_map2object; - tf2::Transform tf_map2future; - - tf2::fromMsg(object.kinematics.pose_with_covariance.pose, tf_map2object); - tf2::fromMsg(object_frame_pose, tf_object2future); - tf_map2future = tf_map2object * tf_object2future; - tf2::toMsg(tf_map2future, map_frame_pose); - double dx = map_frame_pose.position.x - object.kinematics.pose_with_covariance.pose.position.x; - double dy = map_frame_pose.position.y - object.kinematics.pose_with_covariance.pose.position.y; - return std::atan2(dy, dx); -} - -double MapBasedPredictionROS::calculateLikelihood( - const std::vector & path, const TrackedObject & object) -{ - // We compute the confidence value based on the object current position and angle - // Calculate path length - const double path_len = tier4_autoware_utils::calcArcLength(path); - const size_t nearest_segment_idx = tier4_autoware_utils::findNearestSegmentIndex( - path, object.kinematics.pose_with_covariance.pose.position); - const double l = tier4_autoware_utils::calcLongitudinalOffsetToSegment( - path, nearest_segment_idx, object.kinematics.pose_with_covariance.pose.position); - const double current_s_position = - tier4_autoware_utils::calcSignedArcLength(path, 0, nearest_segment_idx) + l; - // If the obstacle is ahead of this path, we assume the confidence for this path is 0 - if (current_s_position > path_len) { - return 0.0; - } - - // Euclid Lateral Distance - const double abs_d = std::fabs(tier4_autoware_utils::calcLateralOffset( - path, object.kinematics.pose_with_covariance.pose.position)); - - // Yaw Difference between obstacle and lane angle - const double lane_yaw = tf2::getYaw(path.at(nearest_segment_idx).orientation); - const double object_yaw = getObjectYaw(object); - const double delta_yaw = object_yaw - lane_yaw; - const double abs_norm_delta_yaw = std::fabs(tier4_autoware_utils::normalizeRadian(delta_yaw)); - - // Compute Chi-squared distributed (Equation (8) in the paper) - const double sigma_d = sigma_lateral_offset_; // Standard Deviation for lateral position - const double sigma_yaw = M_PI * sigma_yaw_angle_ / 180.0; // Standard Deviation for yaw angle - Eigen::Vector2d delta; - delta << abs_d, abs_norm_delta_yaw; - Eigen::Matrix2d P_inv; - P_inv << 1.0 / (sigma_d * sigma_d), 0.0, 0.0, 1.0 / (sigma_yaw * sigma_yaw); - const double MINIMUM_DISTANCE = 1e-6; - const double dist = std::max(delta.dot(P_inv * delta), MINIMUM_DISTANCE); - return 1.0 / dist; -} - -bool MapBasedPredictionROS::checkCloseLaneletCondition( - const std::pair & lanelet, const TrackedObject & object, - const lanelet::BasicPoint2d & search_point) -{ - // Step1. If we only have one point in the centerline, we will ignore the lanelet - if (lanelet.second.centerline().size() <= 1) { - return false; - } - - // Step2. Check if the obstacle is inside of this lanelet - if (!lanelet::geometry::inside(lanelet.second, search_point)) { - return false; - } - - // If the object is in the object buffer, we check if the target lanelet is - // inside the current lanelets id or following lanelets - const std::string object_id = toHexString(object.object_id); - if (object_buffer_.count(object_id) != 0) { - const std::vector & possible_lanelet = - object_buffer_.at(object_id).back().future_possible_lanelets; - - bool not_in_possible_lanelet = - std::find(possible_lanelet.begin(), possible_lanelet.end(), lanelet.second) == - possible_lanelet.end(); - if (not_in_possible_lanelet) { - return false; - } - } - - // Step3. Calculate the angle difference between the lane angle and obstacle angle - double object_yaw = getObjectYaw(object); - const double lane_yaw = lanelet::utils::getLaneletAngle( - lanelet.second, object.kinematics.pose_with_covariance.pose.position); - const double delta_yaw = object_yaw - lane_yaw; - const double normalized_delta_yaw = std::atan2(std::sin(delta_yaw), std::cos(delta_yaw)); - const double abs_norm_delta = std::fabs(normalized_delta_yaw); - - // Step4. Check if the closest lanelet is valid, and add all - // of the lanelets that are below max_dist and max_delta_yaw - if ( - lanelet.first < dist_threshold_for_searching_lanelet_ && - abs_norm_delta < delta_yaw_threshold_for_searching_lanelet_) { - return true; - } - - return false; -} - -bool MapBasedPredictionROS::getClosestLanelets( - const TrackedObject & object, const lanelet::LaneletMapPtr & lanelet_map_ptr_, - lanelet::ConstLanelets & closest_lanelets) -{ - std::chrono::high_resolution_clock::time_point begin = std::chrono::high_resolution_clock::now(); - - // obstacle point - lanelet::BasicPoint2d search_point( - object.kinematics.pose_with_covariance.pose.position.x, - object.kinematics.pose_with_covariance.pose.position.y); - - // nearest lanelet - std::vector> surrounding_lanelets = - lanelet::geometry::findNearest(lanelet_map_ptr_->laneletLayer, search_point, 10); - - std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); - std::chrono::nanoseconds time = std::chrono::duration_cast(end - begin); - debug_accumulated_time_ += time.count() / (1000.0 * 1000.0); - - // No Closest Lanelets - if (surrounding_lanelets.empty()) { - return false; - } - - const double MIN_DIST = 1e-6; - bool found_target_closest_lanelet = false; - for (const auto & lanelet : surrounding_lanelets) { - // Check if the close lanelets meet the necessary condition for start lanelets - if (checkCloseLaneletCondition(lanelet, object, search_point)) { - // If the lanelet meets the condition, - // then check if similar lanelet is inside the closest lanelet - bool is_duplicate = false; - for (const auto & closest_lanelet : closest_lanelets) { - const auto lanelet_end_p = lanelet.second.centerline2d().back(); - const auto closest_lanelet_end_p = closest_lanelet.centerline2d().back(); - const double dist = std::hypot( - lanelet_end_p.x() - closest_lanelet_end_p.x(), - lanelet_end_p.y() - closest_lanelet_end_p.y()); - if (dist < MIN_DIST) { - is_duplicate = true; - break; - } - } - if (is_duplicate) { - continue; - } - - found_target_closest_lanelet = true; - closest_lanelets.push_back(lanelet.second); - } - } - - // If the closest lanelet is valid, return true - return found_target_closest_lanelet; -} - -void MapBasedPredictionROS::removeInvalidObject(const double current_time) -{ - std::vector invalid_object_id; - for (auto iter = object_buffer_.begin(); iter != object_buffer_.end(); ++iter) { - const std::string object_id = iter->first; - std::deque & object_data = iter->second; - - // If object data is empty, we are going to delete the buffer for the obstacle - if (object_data.empty()) { - invalid_object_id.push_back(object_id); - continue; - } - const double latest_object_time = rclcpp::Time(object_data.back().pose.header.stamp).seconds(); - - // Delete Old Objects - if (current_time - latest_object_time > 2.0) { - invalid_object_id.push_back(object_id); - continue; - } - - // Delete old information - while (!object_data.empty()) { - const double post_object_time = rclcpp::Time(object_data.front().pose.header.stamp).seconds(); - if (current_time - post_object_time > 2.0) { - // Delete Old Position - object_data.pop_front(); - } else { - break; - } - } - - if (object_data.empty()) { - invalid_object_id.push_back(object_id); - continue; - } - } - - for (const auto & key : invalid_object_id) { - object_buffer_.erase(key); - } -} - -bool MapBasedPredictionROS::updateObjectBuffer( - const std_msgs::msg::Header & header, const TrackedObject & object, - lanelet::ConstLanelets & current_lanelets) -{ - using Label = ObjectClassification; - // Ignore non-vehicle object - if ( - object.classification.front().label != Label::CAR && - object.classification.front().label != Label::BUS && - object.classification.front().label != Label::TRUCK) { - return false; - } - - // Get the current Lanelet - std::string object_id = toHexString(object.object_id); - - // Check whether the object is on the road - if (!getClosestLanelets(object, lanelet_map_ptr_, current_lanelets)) { - // This Object is not on the road - return false; - } - - // Get current Pose - geometry_msgs::msg::PoseStamped current_object_pose; - current_object_pose.header = header; - current_object_pose.pose = object.kinematics.pose_with_covariance.pose; - - // Update Object Buffer - if (object_buffer_.count(object_id) == 0) { - // New Object - ObjectData single_object_data; - single_object_data.current_lanelets = current_lanelets; - single_object_data.future_possible_lanelets = current_lanelets; - single_object_data.pose = current_object_pose; - const double object_yaw = getObjectYaw(object); - single_object_data.pose.pose.orientation = - tier4_autoware_utils::createQuaternionFromYaw(object_yaw); - - std::deque object_data; - object_data.push_back(single_object_data); - - // Create new key, value pair in the buffer - object_buffer_.emplace(object_id, object_data); - } else { - // Object that is already in the object buffer - std::deque & object_data = object_buffer_.at(object_id); - - ObjectData single_object_data; - single_object_data.current_lanelets = current_lanelets; - single_object_data.future_possible_lanelets = current_lanelets; - single_object_data.pose = current_object_pose; - const double object_yaw = getObjectYaw(object); - single_object_data.pose.pose.orientation = - tier4_autoware_utils::createQuaternionFromYaw(object_yaw); - - // push new object data - object_data.push_back(single_object_data); - } - - // If the obstacle is too slow, we do linear prediction - if ( - std::fabs(object.kinematics.twist_with_covariance.twist.linear.x) < - min_velocity_for_map_based_prediction_) { - return false; - } - - return true; -} - -void MapBasedPredictionROS::updatePossibleLanelets( - const std::string object_id, const lanelet::routing::LaneletPaths & paths) -{ - if (object_buffer_.count(object_id) != 0) { - std::vector & possible_lanelets = - object_buffer_.at(object_id).back().future_possible_lanelets; - for (const auto & path : paths) { - for (const auto & lanelet : path) { - bool not_in_buffer = - std::find(possible_lanelets.begin(), possible_lanelets.end(), lanelet) == - possible_lanelets.end(); - if (not_in_buffer) { - possible_lanelets.push_back(lanelet); - } - } - } - } -} - -void MapBasedPredictionROS::addValidPath( - const lanelet::routing::LaneletPaths & candidate_paths, - lanelet::routing::LaneletPaths & valid_paths) -{ - // Check if candidate paths are already in the valid paths - for (const auto & candidate_path : candidate_paths) { - bool already_searched = false; - for (const auto & valid_path : valid_paths) { - for (const auto & llt : valid_path) { - if (candidate_path.back().id() == llt.id()) { - already_searched = true; - } - } - } - if (!already_searched) { - valid_paths.push_back(candidate_path); - } - } -} - -double MapBasedPredictionROS::calcRightLateralOffset( - const lanelet::ConstLineString2d & bound_line, const geometry_msgs::msg::Pose & search_pose) -{ - std::vector bound_path(bound_line.size()); - for (size_t i = 0; i < bound_path.size(); ++i) { - const double x = bound_line[i].x(); - const double y = bound_line[i].y(); - bound_path[i] = tier4_autoware_utils::createPoint(x, y, 0.0); - } - - return std::fabs(tier4_autoware_utils::calcLateralOffset(bound_path, search_pose.position)); -} - -double MapBasedPredictionROS::calcLeftLateralOffset( - const lanelet::ConstLineString2d & bound_line, const geometry_msgs::msg::Pose & search_pose) -{ - return -calcRightLateralOffset(bound_line, search_pose); -} - -Maneuver MapBasedPredictionROS::detectLaneChange( - const TrackedObject & object, const lanelet::ConstLanelet & current_lanelet, - const double current_time) -{ - // Step1. Check if we have the object in the buffer - const std::string object_id = toHexString(object.object_id); - if (object_buffer_.count(object_id) == 0) { - return Maneuver::LANE_FOLLOW; - } - - // Object History Data - const std::deque object_info = object_buffer_.at(object_id); - - // Step2. Get the previous id - int prev_id = static_cast(object_info.size()) - 1; - while (prev_id >= 0) { - const double prev_time = rclcpp::Time(object_info.at(prev_id).pose.header.stamp).seconds(); - if (current_time - prev_time > history_time_length_) { - break; - } - --prev_id; - } - - if (prev_id < 0) { - return Maneuver::LANE_FOLLOW; - } - - // Step3. Get closest previous lanelet ID - const auto prev_pose = object_info.at(static_cast(prev_id)).pose; - const lanelet::ConstLanelets prev_lanelets = - object_info.at(static_cast(prev_id)).current_lanelets; - if (prev_lanelets.empty()) { - return Maneuver::LANE_FOLLOW; - } - lanelet::ConstLanelet prev_lanelet = prev_lanelets.front(); - double closest_prev_yaw = std::numeric_limits::max(); - for (const auto & lanelet : prev_lanelets) { - const double lane_yaw = lanelet::utils::getLaneletAngle(lanelet, prev_pose.pose.position); - const double delta_yaw = tf2::getYaw(prev_pose.pose.orientation) - lane_yaw; - const double normalized_delta_yaw = tier4_autoware_utils::normalizeRadian(delta_yaw); - if (normalized_delta_yaw < closest_prev_yaw) { - closest_prev_yaw = normalized_delta_yaw; - prev_lanelet = lanelet; - } - } - - // Step4. Check if the vehicle has changed lane - const auto current_pose = object.kinematics.pose_with_covariance.pose; - const double dist = tier4_autoware_utils::calcDistance2d(prev_pose, current_pose); - lanelet::routing::LaneletPaths possible_paths = - routing_graph_ptr_->possiblePaths(prev_lanelet, dist + 2.0, 0, false); - bool has_lane_changed = true; - for (const auto & path : possible_paths) { - for (const auto & lanelet : path) { - if (lanelet == current_lanelet) { - has_lane_changed = false; - break; - } - } - } - - if (has_lane_changed) { - return Maneuver::LANE_FOLLOW; - } - - // Step5. Lane Change Detection - const lanelet::ConstLineString2d prev_left_bound = prev_lanelet.leftBound2d(); - const lanelet::ConstLineString2d prev_right_bound = prev_lanelet.rightBound2d(); - const lanelet::ConstLineString2d current_left_bound = current_lanelet.leftBound2d(); - const lanelet::ConstLineString2d current_right_bound = current_lanelet.rightBound2d(); - const double prev_left_dist = calcLeftLateralOffset(prev_left_bound, prev_pose.pose); - const double prev_right_dist = calcRightLateralOffset(prev_right_bound, prev_pose.pose); - const double current_left_dist = calcLeftLateralOffset(current_left_bound, current_pose); - const double current_right_dist = calcRightLateralOffset(current_right_bound, current_pose); - const double prev_lane_width = std::fabs(prev_left_dist) + std::fabs(prev_right_dist); - const double current_lane_width = std::fabs(current_left_dist) + std::fabs(current_right_dist); - if (prev_lane_width < 1e-3 || current_lane_width < 1e-3) { - RCLCPP_ERROR(get_logger(), "[Map Based Prediction]: Lane Width is too small"); - return Maneuver::LANE_FOLLOW; - } - - const double current_left_dist_ratio = current_left_dist / current_lane_width; - const double current_right_dist_ratio = current_right_dist / current_lane_width; - const double diff_left_current_prev = current_left_dist - prev_left_dist; - const double diff_right_current_prev = current_right_dist - prev_right_dist; - - if ( - current_left_dist_ratio > dist_ratio_threshold_to_left_bound_ && - diff_left_current_prev > diff_dist_threshold_to_left_bound_) { - return Maneuver::LEFT_LANE_CHANGE; - } else if ( - current_right_dist_ratio < dist_ratio_threshold_to_right_bound_ && - diff_right_current_prev < diff_dist_threshold_to_right_bound_) { - return Maneuver::RIGHT_LANE_CHANGE; - } - - return Maneuver::LANE_FOLLOW; -} - -void MapBasedPredictionROS::objectsCallback(const TrackedObjects::ConstSharedPtr in_objects) -{ - debug_accumulated_time_ = 0.0; - std::chrono::high_resolution_clock::time_point begin = std::chrono::high_resolution_clock::now(); - - if (!lanelet_map_ptr_) { - return; - } - - geometry_msgs::msg::TransformStamped world2map_transform; - geometry_msgs::msg::TransformStamped map2world_transform; - geometry_msgs::msg::TransformStamped debug_map2lidar_transform; - try { - world2map_transform = tf_buffer_ptr_->lookupTransform( - "map", // target - in_objects->header.frame_id, // src - in_objects->header.stamp, rclcpp::Duration::from_seconds(0.1)); - map2world_transform = tf_buffer_ptr_->lookupTransform( - in_objects->header.frame_id, // target - "map", // src - in_objects->header.stamp, rclcpp::Duration::from_seconds(0.1)); - debug_map2lidar_transform = tf_buffer_ptr_->lookupTransform( - "base_link", // target - "map", // src - rclcpp::Time(), rclcpp::Duration::from_seconds(0.1)); - } catch (tf2::TransformException & ex) { - RCLCPP_ERROR(get_logger(), "%s", ex.what()); - return; - } - - /////////////////////////////////////////////////////////// - /////////////////// Update Object Buffer ////////////////// - ////////////////////////////////////////////////////////// - const double objects_detected_time = rclcpp::Time(in_objects->header.stamp).seconds(); - removeInvalidObject(objects_detected_time); - - ///////////////////////////////////////////////////////// - ///////////////////// Prediction /////////////////////// - /////////////////////////////////////////////////////// - PredictedObjects objects_without_map; - objects_without_map.header = in_objects->header; - DynamicObjectWithLanesArray prediction_input; - prediction_input.header = in_objects->header; - - for (const auto & object : in_objects->objects) { - std::string object_id = toHexString(object.object_id); - DynamicObjectWithLanes transformed_object; - transformed_object.object = object; - if (in_objects->header.frame_id != "map") { - geometry_msgs::msg::PoseStamped pose_in_map; - geometry_msgs::msg::PoseStamped pose_orig; - pose_orig.pose = object.kinematics.pose_with_covariance.pose; - tf2::doTransform(pose_orig, pose_in_map, world2map_transform); - transformed_object.object.kinematics.pose_with_covariance.pose = pose_in_map.pose; - } - - std_msgs::msg::Header transformed_header = in_objects->header; - transformed_header.frame_id = "map"; - lanelet::ConstLanelets start_lanelets; // current lanelet - if (!updateObjectBuffer(transformed_header, transformed_object.object, start_lanelets)) { - objects_without_map.objects.push_back( - map_based_prediction_->convertToPredictedObject(transformed_object.object)); - continue; - } - - // Obtain valid Paths - const double delta_horizon = 1.0; - const double obj_vel = object.kinematics.twist_with_covariance.twist.linear.x; - lanelet::routing::LaneletPaths paths; - for (const auto & start_lanelet : start_lanelets) { - // Step1. Lane Change Detection - // First: Right to Left Detection Result - // Second: Left to Right Detection Result - const Maneuver maneuver = detectLaneChange(object, start_lanelet, objects_detected_time); - - // Step2. Get the left lanelet - lanelet::routing::LaneletPaths left_paths; - auto opt_left = routing_graph_ptr_->left(start_lanelet); - if (!!opt_left) { - for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { - const double search_dist = horizon * obj_vel + 10.0; - lanelet::routing::LaneletPaths tmp_paths = - routing_graph_ptr_->possiblePaths(*opt_left, search_dist, 0, false); - addValidPath(tmp_paths, left_paths); - } - } - - // Step3. Get the right lanelet - lanelet::routing::LaneletPaths right_paths; - auto opt_right = routing_graph_ptr_->right(start_lanelet); - if (!!opt_right) { - for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { - const double search_dist = horizon * obj_vel + 10.0; - lanelet::routing::LaneletPaths tmp_paths = - routing_graph_ptr_->possiblePaths(*opt_right, search_dist, 0, false); - addValidPath(tmp_paths, right_paths); - } - } - - // Step4. Get the centerline - lanelet::routing::LaneletPaths center_paths; - for (double horizon = prediction_time_horizon_; horizon > 0; horizon -= delta_horizon) { - const double search_dist = horizon * obj_vel + 10.0; - lanelet::routing::LaneletPaths tmp_paths = - routing_graph_ptr_->possiblePaths(start_lanelet, search_dist, 0, false); - addValidPath(tmp_paths, center_paths); - } - - // Step5. Insert Valid Paths - if (!left_paths.empty() && maneuver == Maneuver::LEFT_LANE_CHANGE) { - paths.insert(paths.end(), left_paths.begin(), left_paths.end()); - } else if (!right_paths.empty() && maneuver == Maneuver::RIGHT_LANE_CHANGE) { - paths.insert(paths.end(), right_paths.begin(), right_paths.end()); - } else { - paths.insert(paths.end(), center_paths.begin(), center_paths.end()); - } - } - // If there is no valid path, we'll mark this object as map-less object - if (paths.empty()) { - objects_without_map.objects.push_back( - map_based_prediction_->convertToPredictedObject(transformed_object.object)); - continue; - } - - // Update Possible lanelet in the object buffer - updatePossibleLanelets(object_id, paths); - - std::vector> tmp_paths; - std::vector tmp_confidence; - for (const auto & path : paths) { - std::vector tmp_path; - - // Insert Positions. Note that we insert points from previous lanelet - if (!path.empty()) { - lanelet::ConstLanelets prev_lanelets = routing_graph_ptr_->previous(path.front()); - if (!prev_lanelets.empty()) { - lanelet::ConstLanelet prev_lanelet = prev_lanelets.front(); - for (const auto & point : prev_lanelet.centerline()) { - geometry_msgs::msg::Pose tmp_pose; - tmp_pose.position.x = point.x(); - tmp_pose.position.y = point.y(); - tmp_pose.position.z = point.z(); - tmp_path.push_back(tmp_pose); - } - } - } - - for (const auto & lanelet : path) { - for (const auto & point : lanelet.centerline()) { - geometry_msgs::msg::Pose tmp_pose; - tmp_pose.position.x = point.x(); - tmp_pose.position.y = point.y(); - tmp_pose.position.z = point.z(); - - // Prevent from inserting same points - if (!tmp_path.empty()) { - const auto prev_pose = tmp_path.back(); - const double tmp_dist = tier4_autoware_utils::calcDistance2d(prev_pose, tmp_pose); - if (tmp_dist < 1e-6) { - continue; - } - } - - tmp_path.push_back(tmp_pose); - } - } - - if (tmp_path.size() < 2) { - continue; - } - - // Compute yaw angles - for (size_t pose_id = 0; pose_id < tmp_path.size() - 1; ++pose_id) { - double tmp_yaw = std::atan2( - tmp_path.at(pose_id + 1).position.y - tmp_path.at(pose_id).position.y, - tmp_path.at(pose_id + 1).position.x - tmp_path.at(pose_id).position.x); - tf2::Quaternion quat; - quat.setRPY(0.0, 0.0, tmp_yaw); - tmp_path.at(pose_id).orientation = tf2::toMsg(quat); - } - tmp_path.back().orientation = tmp_path.at(tmp_path.size() - 2).orientation; - - ////////////////////////////////////////////////////////////////////// - // Calculate Confidence of each path(centerline) for this obstacle // - //////////////////////////////////////////////////////////////////// - const double confidence = calculateLikelihood(tmp_path, transformed_object.object); - // Ignore a path that has too low confidence - if (confidence < 1e-6) { - continue; - } - - tmp_paths.push_back(tmp_path); - tmp_confidence.push_back(confidence); - } - - transformed_object.lanes = tmp_paths; - transformed_object.confidence = tmp_confidence; - prediction_input.objects.push_back(transformed_object); - } - - std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); - std::chrono::nanoseconds time = std::chrono::duration_cast(end - begin); - - std::vector out_objects_in_map; - map_based_prediction_->doPrediction(prediction_input, out_objects_in_map); - PredictedObjects output; - output.header = in_objects->header; - output.header.frame_id = "map"; - output.objects = out_objects_in_map; - - std::vector out_objects_without_map; - map_based_prediction_->doLinearPrediction(objects_without_map, out_objects_without_map); - output.objects.insert( - output.objects.begin(), out_objects_without_map.begin(), out_objects_without_map.end()); - pub_objects_->publish(output); -} - -void MapBasedPredictionROS::mapCallback(const HADMapBin::ConstSharedPtr msg) -{ - RCLCPP_INFO(get_logger(), "Start loading lanelet"); - lanelet_map_ptr_ = std::make_shared(); - lanelet::utils::conversion::fromBinMsg( - *msg, lanelet_map_ptr_, &traffic_rules_ptr_, &routing_graph_ptr_); - RCLCPP_INFO(get_logger(), "Map is loaded"); -} -} // namespace map_based_prediction - -#include -RCLCPP_COMPONENTS_REGISTER_NODE(map_based_prediction::MapBasedPredictionROS) diff --git a/perception/map_based_prediction/src/path_generator.cpp b/perception/map_based_prediction/src/path_generator.cpp new file mode 100644 index 0000000000000..9f28f113b194b --- /dev/null +++ b/perception/map_based_prediction/src/path_generator.cpp @@ -0,0 +1,305 @@ +// Copyright 2021 Tier IV, Inc. +// +// 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 "map_based_prediction/path_generator.hpp" + +#include +#include + +#include + +namespace map_based_prediction +{ +PathGenerator::PathGenerator(const double time_horizon, const double sampling_time_interval) +: time_horizon_(time_horizon), sampling_time_interval_(sampling_time_interval) +{ +} + +PredictedPath PathGenerator::generatePathForNonVehicleObject(const TrackedObject & object) +{ + return generateStraightPath(object); +} + +PredictedPath PathGenerator::generatePathForLowSpeedVehicle(const TrackedObject & object) const +{ + PredictedPath path; + path.time_step = rclcpp::Duration::from_seconds(sampling_time_interval_); + const double ep = 0.001; + for (double dt = 0.0; dt < time_horizon_ + ep; dt += sampling_time_interval_) { + path.path.push_back(object.kinematics.pose_with_covariance.pose); + } + + return path; +} + +PredictedPath PathGenerator::generatePathForOffLaneVehicle(const TrackedObject & object) +{ + return generateStraightPath(object); +} + +PredictedPath PathGenerator::generatePathForOnLaneVehicle( + const TrackedObject & object, const PosePath & ref_paths) +{ + if (ref_paths.size() < 2) { + const PredictedPath empty_path; + return empty_path; + } + + return generatePolynomialPath(object, ref_paths); +} + +PredictedPath PathGenerator::generateStraightPath(const TrackedObject & object) const +{ + const auto & object_pose = object.kinematics.pose_with_covariance.pose; + const auto & object_twist = object.kinematics.twist_with_covariance.twist; + const double ep = 0.001; + const double duration = time_horizon_ + ep; + + PredictedPath path; + path.time_step = rclcpp::Duration::from_seconds(sampling_time_interval_); + path.path.reserve(static_cast((duration) / sampling_time_interval_)); + for (double dt = 0.0; dt < duration; dt += sampling_time_interval_) { + const auto future_obj_pose = tier4_autoware_utils::calcOffsetPose( + object_pose, object_twist.linear.x * dt, object_twist.linear.y * dt, 0.0); + path.path.push_back(future_obj_pose); + } + + return path; +} + +PredictedPath PathGenerator::generatePolynomialPath( + const TrackedObject & object, const PosePath & ref_path) +{ + // Get current Frenet Point + const double ref_path_len = tier4_autoware_utils::calcArcLength(ref_path); + const auto current_point = getFrenetPoint(object, ref_path); + + // Step1. Set Target Frenet Point + // Note that we do not set position s, + // since we don't know the target longitudinal position + FrenetPoint terminal_point; + terminal_point.s_vel = current_point.s_vel; + terminal_point.s_acc = 0.0; + terminal_point.d = 0.0; + terminal_point.d_vel = 0.0; + terminal_point.d_acc = 0.0; + + // Step2. Generate Predicted Path on a Frenet coordinate + const auto frenet_predicted_path = + generateFrenetPath(current_point, terminal_point, ref_path_len); + + // Step3. Interpolate Reference Path for converting predicted path coordinate + const auto interpolated_ref_path = interpolateReferencePath(ref_path, frenet_predicted_path); + + if (frenet_predicted_path.size() < 2 || interpolated_ref_path.size() < 2) { + const PredictedPath empty_path; + return empty_path; + } + + // Step4. Convert predicted trajectory from Frenet to Cartesian coordinate + return convertToPredictedPath(object, frenet_predicted_path, interpolated_ref_path); +} + +FrenetPath PathGenerator::generateFrenetPath( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double max_length) +{ + FrenetPath path; + const double duration = time_horizon_; + + // Compute Lateral and Longitudinal Coefficients to generate the trajectory + const Eigen::Vector3d lat_coeff = calcLatCoefficients(current_point, target_point, duration); + const Eigen::Vector2d lon_coeff = calcLonCoefficients(current_point, target_point, duration); + + path.reserve(static_cast(duration / sampling_time_interval_)); + for (double t = 0.0; t <= duration; t += sampling_time_interval_) { + const double d_next = current_point.d + current_point.d_vel * t + 0 * 2 * std::pow(t, 2) + + lat_coeff(0) * std::pow(t, 3) + lat_coeff(1) * std::pow(t, 4) + + lat_coeff(2) * std::pow(t, 5); + const double s_next = current_point.s + current_point.s_vel * t + 2 * 0 * std::pow(t, 2) + + lon_coeff(0) * std::pow(t, 3) + lon_coeff(1) * std::pow(t, 4); + if (s_next > max_length) { + break; + } + + // We assume the object is traveling at a constant speed along s direction + FrenetPoint point; + point.s = std::max(s_next, 0.0); + point.s_vel = current_point.s_vel; + point.s_acc = current_point.s_acc; + point.d = d_next; + point.d_vel = current_point.d_vel; + point.d_acc = current_point.d_acc; + path.push_back(point); + } + + return path; +} + +Eigen::Vector3d PathGenerator::calcLatCoefficients( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double T) +{ + // Lateral Path Calculation + // Quintic polynomial for d + // A = np.array([[T**3, T**4, T**5], + // [3 * T ** 2, 4 * T ** 3, 5 * T ** 4], + // [6 * T, 12 * T ** 2, 20 * T ** 3]]) + // A_inv = np.matrix([[10/(T**3), -4/(T**2), 1/(2*T)], + // [-15/(T**4), 7/(T**3), -1/(T**2)], + // [6/(T**5), -3/(T**4), 1/(2*T**3)]]) + // b = np.matrix([[xe - self.a0 - self.a1 * T - self.a2 * T**2], + // [vxe - self.a1 - 2 * self.a2 * T], + // [axe - 2 * self.a2]]) + Eigen::Matrix3d A_lat_inv; + A_lat_inv << 10 / std::pow(T, 3), -4 / std::pow(T, 2), 1 / (2 * T), -15 / std::pow(T, 4), + 7 / std::pow(T, 3), -1 / std::pow(T, 2), 6 / std::pow(T, 5), -3 / std::pow(T, 4), + 1 / (2 * std::pow(T, 3)); + Eigen::Vector3d b_lat; + b_lat[0] = target_point.d - current_point.d - current_point.d_vel * T; + b_lat[1] = target_point.d_vel - current_point.d_vel; + b_lat[2] = target_point.d_acc; + + return A_lat_inv * b_lat; +} + +Eigen::Vector2d PathGenerator::calcLonCoefficients( + const FrenetPoint & current_point, const FrenetPoint & target_point, const double T) +{ + // Longitudinal Path Calculation + // Quadric polynomial + // A_inv = np.matrix([[1/(T**2), -1/(3*T)], + // [-1/(2*T**3), 1/(4*T**2)]]) + // b = np.matrix([[vxe - self.a1 - 2 * self.a2 * T], + // [axe - 2 * self.a2]]) + Eigen::Matrix2d A_lon_inv; + A_lon_inv << 1 / std::pow(T, 2), -1 / (3 * T), -1 / (2 * std::pow(T, 3)), + 1 / (4 * std::pow(T, 2)); + Eigen::Vector2d b_lon; + b_lon[0] = target_point.s_vel - current_point.s_vel; + b_lon[1] = 0.0; + return A_lon_inv * b_lon; +} + +PosePath PathGenerator::interpolateReferencePath( + const PosePath & base_path, const FrenetPath & frenet_predicted_path) +{ + PosePath interpolated_path; + const size_t interpolate_num = frenet_predicted_path.size(); + if (interpolate_num < 2) { + interpolated_path.emplace_back(base_path.front()); + return interpolated_path; + } + + std::vector base_path_x; + std::vector base_path_y; + std::vector base_path_z; + std::vector base_path_s; + for (size_t i = 0; i < base_path.size(); ++i) { + base_path_x.push_back(base_path.at(i).position.x); + base_path_y.push_back(base_path.at(i).position.y); + base_path_z.push_back(base_path.at(i).position.z); + base_path_s.push_back(tier4_autoware_utils::calcSignedArcLength(base_path, 0, i)); + } + + std::vector resampled_s(frenet_predicted_path.size()); + for (size_t i = 0; i < frenet_predicted_path.size(); ++i) { + resampled_s.at(i) = frenet_predicted_path.at(i).s; + } + if (resampled_s.front() > resampled_s.back()) { + std::reverse(resampled_s.begin(), resampled_s.end()); + } + + // Spline Interpolation + std::vector slerp_ref_path_x = + interpolation::slerp(base_path_s, base_path_x, resampled_s); + std::vector slerp_ref_path_y = + interpolation::slerp(base_path_s, base_path_y, resampled_s); + std::vector slerp_ref_path_z = + interpolation::slerp(base_path_s, base_path_z, resampled_s); + + interpolated_path.resize(interpolate_num); + for (size_t i = 0; i < interpolate_num - 1; ++i) { + geometry_msgs::msg::Pose interpolated_pose; + const auto current_point = + tier4_autoware_utils::createPoint(slerp_ref_path_x.at(i), slerp_ref_path_y.at(i), 0.0); + const auto next_point = tier4_autoware_utils::createPoint( + slerp_ref_path_x.at(i + 1), slerp_ref_path_y.at(i + 1), 0.0); + const double yaw = tier4_autoware_utils::calcAzimuthAngle(current_point, next_point); + interpolated_pose.position = tier4_autoware_utils::createPoint( + slerp_ref_path_x.at(i), slerp_ref_path_y.at(i), slerp_ref_path_z.at(i)); + interpolated_pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(yaw); + interpolated_path.at(i) = interpolated_pose; + } + interpolated_path.back().position = tier4_autoware_utils::createPoint( + slerp_ref_path_x.back(), slerp_ref_path_y.back(), slerp_ref_path_z.back()); + interpolated_path.back().orientation = interpolated_path.at(interpolate_num - 2).orientation; + + return interpolated_path; +} + +PredictedPath PathGenerator::convertToPredictedPath( + const TrackedObject & object, const FrenetPath & frenet_predicted_path, const PosePath & ref_path) +{ + PredictedPath predicted_path; + predicted_path.time_step = rclcpp::Duration::from_seconds(sampling_time_interval_); + predicted_path.path.resize(ref_path.size()); + for (size_t i = 0; i < predicted_path.path.size(); ++i) { + // Reference Point from interpolated reference path + const auto & ref_pose = ref_path.at(i); + + // Frenet Point from frenet predicted path + const auto & frenet_point = frenet_predicted_path.at(i); + + // Converted Pose + auto predicted_pose = tier4_autoware_utils::calcOffsetPose(ref_pose, 0.0, frenet_point.d, 0.0); + predicted_pose.position.z = object.kinematics.pose_with_covariance.pose.position.z; + if (i == 0) { + predicted_pose.orientation = object.kinematics.pose_with_covariance.pose.orientation; + } else { + const double yaw = tier4_autoware_utils::calcAzimuthAngle( + predicted_path.path.at(i - 1).position, predicted_pose.position); + predicted_pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(yaw); + } + predicted_path.path.at(i) = predicted_pose; + } + + return predicted_path; +} + +FrenetPoint PathGenerator::getFrenetPoint(const TrackedObject & object, const PosePath & ref_path) +{ + FrenetPoint frenet_point; + const auto obj_point = object.kinematics.pose_with_covariance.pose.position; + + const size_t nearest_segment_idx = + tier4_autoware_utils::findNearestSegmentIndex(ref_path, obj_point); + const double l = + tier4_autoware_utils::calcLongitudinalOffsetToSegment(ref_path, nearest_segment_idx, obj_point); + const float vx = static_cast(object.kinematics.twist_with_covariance.twist.linear.x); + const float vy = static_cast(object.kinematics.twist_with_covariance.twist.linear.y); + const float obj_yaw = + static_cast(tf2::getYaw(object.kinematics.pose_with_covariance.pose.orientation)); + const float lane_yaw = + static_cast(tf2::getYaw(ref_path.at(nearest_segment_idx).orientation)); + const float delta_yaw = obj_yaw - lane_yaw; + + frenet_point.s = tier4_autoware_utils::calcSignedArcLength(ref_path, 0, nearest_segment_idx) + l; + frenet_point.d = tier4_autoware_utils::calcLateralOffset(ref_path, obj_point); + frenet_point.s_vel = vx * std::cos(delta_yaw) - vy * std::sin(delta_yaw); + frenet_point.d_vel = vx * std::sin(delta_yaw) + vy * std::cos(delta_yaw); + frenet_point.s_acc = 0.0; + frenet_point.d_acc = 0.0; + + return frenet_point; +} +} // namespace map_based_prediction From 098d3241741e7daeffef9f1bcb1c5d23ed2968ef Mon Sep 17 00:00:00 2001 From: Yutaka Shimizu <43805014+purewater0901@users.noreply.github.com> Date: Fri, 18 Mar 2022 15:51:31 +0900 Subject: [PATCH 11/16] feat(map_based_prediction): add prediction parameter (#545) * use autoware utils * add parameter file * revert change --- .../config/map_based_prediction.param.yaml | 17 ++++++++++ .../launch/map_based_prediction.launch.xml | 34 +++---------------- 2 files changed, 21 insertions(+), 30 deletions(-) create mode 100644 perception/map_based_prediction/config/map_based_prediction.param.yaml diff --git a/perception/map_based_prediction/config/map_based_prediction.param.yaml b/perception/map_based_prediction/config/map_based_prediction.param.yaml new file mode 100644 index 0000000000000..ef67fa7b00aed --- /dev/null +++ b/perception/map_based_prediction/config/map_based_prediction.param.yaml @@ -0,0 +1,17 @@ +/**: + ros__parameters: + enable_delay_compensation: true + prediction_time_horizon: 10.0 #[s] + prediction_sampling_delta_time: 0.5 #[s] + min_velocity_for_map_based_prediction: 1.0 #[m/s] + dist_threshold_for_searching_lanelet: 3.0 #[m] + delta_yaw_threshold_for_searching_lanelet: 0.785 #[rad] + sigma_lateral_offset: 0.5 #[m] + sigma_yaw_angle_deg: 5.0 #[angle degree] + object_buffer_time_length: 2.0 #[s] + history_time_length: 1.0 #[s] + dist_ratio_threshold_to_left_bound: -0.5 #[ratio] + dist_ratio_threshold_to_right_bound: 0.5 #[ratio + diff_dist_threshold_to_left_bound: 0.29 #[m] + diff_dist_threshold_to_right_bound: -0.29 #[m] + reference_path_resolution: 0.5 #[m] diff --git a/perception/map_based_prediction/launch/map_based_prediction.launch.xml b/perception/map_based_prediction/launch/map_based_prediction.launch.xml index 7c917be0fd687..884b4947b3571 100644 --- a/perception/map_based_prediction/launch/map_based_prediction.launch.xml +++ b/perception/map_based_prediction/launch/map_based_prediction.launch.xml @@ -1,39 +1,13 @@ - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - + From 06cccdd2bfac1b05c4ac2bd5f1cd723fc549c756 Mon Sep 17 00:00:00 2001 From: Takayuki Murooka Date: Fri, 18 Mar 2022 16:02:39 +0900 Subject: [PATCH 12/16] feat(obstacle_avoidance_planner): split debug marker, and removed unnecessary files (#543) Signed-off-by: Takayuki Murooka --- .../obstacle_avoidance_planner/debug.hpp | 105 --- .../debug_visualization.hpp | 3 + .../obstacle_avoidance_planner/node.hpp | 1 + .../obstacle_avoidance_planner/src/debug.cpp | 780 ------------------ .../src/debug_visualization.cpp | 26 +- .../obstacle_avoidance_planner/src/node.cpp | 15 + 6 files changed, 35 insertions(+), 895 deletions(-) delete mode 100644 planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug.hpp delete mode 100644 planning/obstacle_avoidance_planner/src/debug.cpp diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug.hpp deleted file mode 100644 index 9baf1b686fa7d..0000000000000 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug.hpp +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020 Tier IV, Inc. -// -// 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 OBSTACLE_AVOIDANCE_PLANNER__DEBUG_HPP_ -#define OBSTACLE_AVOIDANCE_PLANNER__DEBUG_HPP_ - -#include - -#include -#include -#include -#include - -#include -#include - -struct ConstrainRectangle; -struct Bounds; -struct DebugData; -struct VehicleParam; - -namespace util -{ -struct Footprint; -} - -visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( - const DebugData & debug_data, - const std::vector & optimized_points, - const VehicleParam & vehicle_param); - -geometry_msgs::msg::Pose getVirtualWallPose( - const geometry_msgs::msg::Pose & target_pose, const VehicleParam & vehicle_param); - -visualization_msgs::msg::MarkerArray getDebugPointsMarkers( - const std::vector & interpolated_points, - const std::vector & optimized_points, - const std::vector & straight_points, - const std::vector & fixed_points, - const std::vector & non_fixed_points); - -visualization_msgs::msg::MarkerArray getDebugConstrainMarkers( - const std::vector & constrain_ranges, const std::string & ns); - -visualization_msgs::msg::MarkerArray getObjectsMarkerArray( - const std::vector & objects, - const std::string & ns, const double r, const double g, const double b); - -visualization_msgs::msg::MarkerArray getRectanglesMarkerArray( - const std::vector & rects, const std::string & ns, const double r, - const double g, const double b); - -visualization_msgs::msg::MarkerArray getRectanglesNumMarkerArray( - const std::vector & rects, const std::string & ns, const double r, - const double g, const double b); - -visualization_msgs::msg::MarkerArray getPointsMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b); - -visualization_msgs::msg::MarkerArray getPointsMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b); - -visualization_msgs::msg::MarkerArray getPointsTextMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b); - -visualization_msgs::msg::MarkerArray getPointsTextMarkerArray( - const std::vector & points, - const std::string & ns, const double r, const double g, const double b); - -visualization_msgs::msg::MarkerArray getBaseBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_p0, - const std::string & ns, const double r, const double g, const double b); - -visualization_msgs::msg::MarkerArray getTopBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_p1, - const std::string & ns, const double r, const double g, const double b); - -visualization_msgs::msg::MarkerArray getMidBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_top, - const std::string & ns, const double r, const double g, const double b); - -visualization_msgs::msg::MarkerArray getVirtualWallMarkerArray( - const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, - const double b); - -visualization_msgs::msg::MarkerArray getVirtualWallTextMarkerArray( - const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, - const double b); - -nav_msgs::msg::OccupancyGrid getDebugCostmap( - const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid); -#endif // OBSTACLE_AVOIDANCE_PLANNER__DEBUG_HPP_ diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp index ead547892ee24..ce7bda4bd1c7f 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/debug_visualization.hpp @@ -40,6 +40,9 @@ visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( const std::vector & optimized_points, const VehicleParam & vehicle_param, const bool is_showing_debug_detail); +visualization_msgs::msg::MarkerArray getDebugVisualizationWallMarker( + const std::shared_ptr debug_data_ptr, const VehicleParam & vehicle_param); + nav_msgs::msg::OccupancyGrid getDebugCostmap( const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid); } // namespace debug_visualization diff --git a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp index 56f4e437853f3..6f38883aa4790 100644 --- a/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp +++ b/planning/obstacle_avoidance_planner/include/obstacle_avoidance_planner/node.hpp @@ -167,6 +167,7 @@ class ObstacleAvoidancePlanner : public rclcpp::Node rclcpp::Publisher::SharedPtr debug_mpt_traj_pub_; rclcpp::Publisher::SharedPtr debug_markers_pub_; + rclcpp::Publisher::SharedPtr debug_wall_markers_pub_; rclcpp::Publisher::SharedPtr debug_clearance_map_pub_; rclcpp::Publisher::SharedPtr debug_object_clearance_map_pub_; rclcpp::Publisher::SharedPtr debug_area_with_objects_pub_; diff --git a/planning/obstacle_avoidance_planner/src/debug.cpp b/planning/obstacle_avoidance_planner/src/debug.cpp deleted file mode 100644 index 5b591c6412224..0000000000000 --- a/planning/obstacle_avoidance_planner/src/debug.cpp +++ /dev/null @@ -1,780 +0,0 @@ -// Copyright 2020 Tier IV, Inc. -// -// 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 "obstacle_avoidance_planner/debug.hpp" - -#include "obstacle_avoidance_planner/eb_path_optimizer.hpp" -#include "obstacle_avoidance_planner/marker_helper.hpp" -#include "obstacle_avoidance_planner/mpt_optimizer.hpp" -#include "obstacle_avoidance_planner/process_cv.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( - const DebugData & debug_data, - // const std::vector & interpolated_points, - // const std::vector & smoothed_points, - const std::vector & optimized_points, - const VehicleParam & vehicle_param) -{ - const auto points_marker_array = getDebugPointsMarkers( - debug_data.interpolated_points, optimized_points, debug_data.straight_points, - debug_data.fixed_points, debug_data.non_fixed_points); - const auto constrain_marker_array = - getDebugConstrainMarkers(debug_data.constrain_rectangles, "constrain_rect"); - const auto extending_constrain_marker_array = getDebugConstrainMarkers( - debug_data.constrain_rectangles_for_extending, "extending_constrain_rect"); - - visualization_msgs::msg::MarkerArray vis_marker_array; - if (debug_data.is_expected_to_over_drivable_area && !optimized_points.empty()) { - const auto virtual_wall_pose = getVirtualWallPose(optimized_points.back().pose, vehicle_param); - appendMarkerArray( - getVirtualWallMarkerArray(virtual_wall_pose, "virtual_wall", 1.0, 0, 0), &vis_marker_array); - appendMarkerArray( - getVirtualWallTextMarkerArray(virtual_wall_pose, "virtual_wall_text", 1.0, 1.0, 1.0), - &vis_marker_array); - } - appendMarkerArray(points_marker_array, &vis_marker_array); - appendMarkerArray(constrain_marker_array, &vis_marker_array); - appendMarkerArray(extending_constrain_marker_array, &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray( - debug_data.fixed_points_for_extending, "fixed_points_for_extending", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray( - debug_data.non_fixed_points_for_extending, "non_fixed_points_for_extending", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray( - debug_data.interpolated_points_for_extending, "interpolated_points_for_extending", 0.99, 0.99, - 0.2), - &vis_marker_array); - appendMarkerArray( - getObjectsMarkerArray(debug_data.avoiding_objects, "avoiding_objects", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getRectanglesMarkerArray(debug_data.vehicle_footprints, "vehicle_footprint", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getRectanglesMarkerArray( - debug_data.current_vehicle_footprints, "current_vehicle_footprint", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getRectanglesNumMarkerArray( - debug_data.vehicle_footprints, "num_vehicle_footprint", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray( - debug_data.bounds_candidate_for_base_points, "bounds_candidate_for_base", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray( - debug_data.bounds_candidate_for_top_points, "bounds_candidate_for_top", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsMarkerArray(debug_data.fixed_mpt_points, "fixed_mpt_points", 0.99, 0.0, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsTextMarkerArray( - debug_data.bounds_candidate_for_base_points, "bounds_candidate_base_text", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsTextMarkerArray( - debug_data.bounds_candidate_for_top_points, "bounds_candidate_top_text", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getPointsTextMarkerArray(debug_data.smoothed_points, "smoothed_points_text", 0.99, 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getBaseBoundsLineMarkerArray( - debug_data.bounds, debug_data.bounds_candidate_for_base_points, "base_bounds_line", 0.99, - 0.99, 0.2), - &vis_marker_array); - appendMarkerArray( - getTopBoundsLineMarkerArray( - debug_data.bounds, debug_data.bounds_candidate_for_top_points, "top_bounds_line", 0.99, 0.99, - 0.2), - &vis_marker_array); - appendMarkerArray( - getMidBoundsLineMarkerArray( - debug_data.bounds, debug_data.bounds_candidate_for_mid_points, "mid_bounds_line", 0.99, 0.99, - 0.2), - &vis_marker_array); - - return vis_marker_array; -} - -geometry_msgs::msg::Pose getVirtualWallPose( - const geometry_msgs::msg::Pose & target_pose, const VehicleParam & vehicle_param) -{ - const double base_link2front = vehicle_param.wheelbase + vehicle_param.front_overhang; - tf2::Transform tf_base_link2front( - tf2::Quaternion(0.0, 0.0, 0.0, 1.0), tf2::Vector3(base_link2front, 0.0, 0.0)); - tf2::Transform tf_map2base_link; - tf2::fromMsg(target_pose, tf_map2base_link); - tf2::Transform tf_map2front = tf_map2base_link * tf_base_link2front; - geometry_msgs::msg::Pose virtual_wall_pose; - tf2::toMsg(tf_map2front, virtual_wall_pose); - return virtual_wall_pose; -} - -visualization_msgs::msg::MarkerArray getDebugPointsMarkers( - const std::vector & interpolated_points, - const std::vector & optimized_points, - const std::vector & straight_points, - const std::vector & fixed_points, - const std::vector & non_fixed_points) -{ - visualization_msgs::msg::MarkerArray marker_array; - int unique_id = 0; - - unique_id = 0; - visualization_msgs::msg::Marker interpolated_points_marker; - interpolated_points_marker.lifetime = rclcpp::Duration::from_seconds(0); - interpolated_points_marker.header.frame_id = "map"; - interpolated_points_marker.header.stamp = rclcpp::Time(0); - interpolated_points_marker.ns = std::string("interpolated_points_marker"); - interpolated_points_marker.action = visualization_msgs::msg::Marker::ADD; - interpolated_points_marker.pose.orientation.w = 1.0; - interpolated_points_marker.id = unique_id; - interpolated_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - interpolated_points_marker.scale = createMarkerScale(.3, .3, .3); - interpolated_points_marker.color = createMarkerColor(1.0f, 0, 0, 0.8); - unique_id++; - for (const auto & point : interpolated_points) { - interpolated_points_marker.points.push_back(point); - } - if (!interpolated_points_marker.points.empty()) { - marker_array.markers.push_back(interpolated_points_marker); - } - - unique_id = 0; - visualization_msgs::msg::Marker optimized_points_marker; - optimized_points_marker.lifetime = rclcpp::Duration::from_seconds(0); - optimized_points_marker.header.frame_id = "map"; - optimized_points_marker.header.stamp = rclcpp::Time(0); - optimized_points_marker.ns = std::string("optimized_points_marker"); - optimized_points_marker.action = visualization_msgs::msg::Marker::ADD; - optimized_points_marker.pose.orientation.w = 1.0; - optimized_points_marker.id = unique_id; - optimized_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - optimized_points_marker.scale = createMarkerScale(0.35, 0.35, 0.35); - optimized_points_marker.color = createMarkerColor(0, 1.0f, 0, 0.99); - unique_id++; - for (const auto & point : optimized_points) { - optimized_points_marker.points.push_back(point.pose.position); - } - if (!optimized_points_marker.points.empty()) { - marker_array.markers.push_back(optimized_points_marker); - } - - unique_id = 0; - for (std::size_t i = 0; i < optimized_points.size(); i++) { - visualization_msgs::msg::Marker optimized_points_text_marker; - optimized_points_text_marker.lifetime = rclcpp::Duration::from_seconds(0); - optimized_points_text_marker.header.frame_id = "map"; - optimized_points_text_marker.header.stamp = rclcpp::Time(0); - optimized_points_text_marker.ns = std::string("optimized_points_text_marker"); - optimized_points_text_marker.action = visualization_msgs::msg::Marker::ADD; - optimized_points_text_marker.pose.orientation.w = 1.0; - optimized_points_text_marker.id = unique_id; - optimized_points_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - optimized_points_text_marker.pose.position = optimized_points[i].pose.position; - optimized_points_text_marker.scale = createMarkerScale(0, 0, 0.15); - optimized_points_text_marker.color = createMarkerColor(0, 1.0, 0, 0.99); - optimized_points_text_marker.text = std::to_string(i); - unique_id++; - marker_array.markers.push_back(optimized_points_text_marker); - } - - unique_id = 0; - for (std::size_t i = 0; i < interpolated_points.size(); i++) { - visualization_msgs::msg::Marker interpolated_points_text_marker; - interpolated_points_text_marker.lifetime = rclcpp::Duration::from_seconds(0); - interpolated_points_text_marker.header.frame_id = "map"; - interpolated_points_text_marker.header.stamp = rclcpp::Time(0); - interpolated_points_text_marker.ns = std::string("interpolated_points_text_marker"); - interpolated_points_text_marker.action = visualization_msgs::msg::Marker::ADD; - interpolated_points_text_marker.pose.orientation.w = 1.0; - interpolated_points_text_marker.id = unique_id; - interpolated_points_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - interpolated_points_text_marker.pose.position = interpolated_points[i]; - interpolated_points_text_marker.scale = createMarkerScale(0, 0, 0.5); - interpolated_points_text_marker.color = createMarkerColor(0, 1.0, 0, 0.99); - interpolated_points_text_marker.text = std::to_string(i); - unique_id++; - marker_array.markers.push_back(interpolated_points_text_marker); - } - - unique_id = 0; - visualization_msgs::msg::Marker straight_points_marker; - straight_points_marker.lifetime = rclcpp::Duration::from_seconds(40); - straight_points_marker.header.frame_id = "map"; - straight_points_marker.header.stamp = rclcpp::Time(0); - straight_points_marker.ns = std::string("straight_points_marker"); - straight_points_marker.action = visualization_msgs::msg::Marker::ADD; - straight_points_marker.pose.orientation.w = 1.0; - straight_points_marker.id = unique_id; - straight_points_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - straight_points_marker.scale = createMarkerScale(1.0, 1.0, 1.0); - straight_points_marker.color = createMarkerColor(1.0, 1.0, 0, 0.99); - unique_id++; - for (const auto & point : straight_points) { - straight_points_marker.points.push_back(point); - } - if (!straight_points_marker.points.empty()) { - marker_array.markers.push_back(straight_points_marker); - } - - unique_id = 0; - visualization_msgs::msg::Marker fixed_marker; - fixed_marker.lifetime = rclcpp::Duration::from_seconds(0); - fixed_marker.header.frame_id = "map"; - fixed_marker.header.stamp = rclcpp::Time(0); - fixed_marker.ns = std::string("fixed_points_marker"); - fixed_marker.action = visualization_msgs::msg::Marker::ADD; - fixed_marker.pose.orientation.w = 1.0; - fixed_marker.id = unique_id; - fixed_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - fixed_marker.scale = createMarkerScale(0.3, 0.3, 0.3); - fixed_marker.color = createMarkerColor(1.0, 0, 0, 0.99); - unique_id++; - for (const auto & point : fixed_points) { - fixed_marker.points.push_back(point.position); - } - if (!fixed_marker.points.empty()) { - marker_array.markers.push_back(fixed_marker); - } - - visualization_msgs::msg::Marker non_fixed_marker; - non_fixed_marker.lifetime = rclcpp::Duration::from_seconds(20); - non_fixed_marker.header.frame_id = "map"; - non_fixed_marker.header.stamp = rclcpp::Time(0); - non_fixed_marker.ns = std::string("non_fixed_points_marker"); - non_fixed_marker.action = visualization_msgs::msg::Marker::ADD; - non_fixed_marker.pose.orientation.w = 1.0; - non_fixed_marker.id = unique_id; - non_fixed_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - non_fixed_marker.scale = createMarkerScale(1.0, 1.0, 1.0); - non_fixed_marker.color = createMarkerColor(0, 1.0, 0, 0.99); - unique_id++; - for (const auto & point : non_fixed_points) { - non_fixed_marker.points.push_back(point.position); - } - if (!non_fixed_marker.points.empty()) { - marker_array.markers.push_back(non_fixed_marker); - } - return marker_array; -} - -visualization_msgs::msg::MarkerArray getDebugConstrainMarkers( - const std::vector & constrain_ranges, const std::string & ns) -{ - visualization_msgs::msg::MarkerArray marker_array; - int unique_id = 0; - for (std::size_t i = 0; i < constrain_ranges.size(); i++) { - visualization_msgs::msg::Marker constrain_rect_marker; - constrain_rect_marker.lifetime = rclcpp::Duration::from_seconds(0); - constrain_rect_marker.header.frame_id = "map"; - constrain_rect_marker.header.stamp = rclcpp::Time(0); - constrain_rect_marker.ns = ns; - constrain_rect_marker.action = visualization_msgs::msg::Marker::ADD; - constrain_rect_marker.pose.orientation.w = 1.0; - constrain_rect_marker.id = unique_id; - constrain_rect_marker.type = visualization_msgs::msg::Marker::LINE_STRIP; - constrain_rect_marker.scale = createMarkerScale(0.01, 0, 0); - constrain_rect_marker.color = createMarkerColor(1.0, 0, 0, 0.99); - unique_id++; - geometry_msgs::msg::Point top_left_point = constrain_ranges[i].top_left; - geometry_msgs::msg::Point top_right_point = constrain_ranges[i].top_right; - geometry_msgs::msg::Point bottom_right_point = constrain_ranges[i].bottom_right; - geometry_msgs::msg::Point bottom_left_point = constrain_ranges[i].bottom_left; - constrain_rect_marker.points.push_back(top_left_point); - constrain_rect_marker.points.push_back(top_right_point); - constrain_rect_marker.points.push_back(bottom_right_point); - constrain_rect_marker.points.push_back(bottom_left_point); - constrain_rect_marker.points.push_back(top_left_point); - marker_array.markers.push_back(constrain_rect_marker); - } - - for (std::size_t i = 0; i < constrain_ranges.size(); i++) { - visualization_msgs::msg::Marker marker; - marker.header.frame_id = "map"; - marker.header.stamp = rclcpp::Time(0); - marker.ns = ns + "_text"; - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - marker.scale = createMarkerScale(0, 0, 0.15); - marker.color = createMarkerColor(1.0, 0, 0, 0.99); - marker.text = std::to_string(i); - marker.pose.position = constrain_ranges[i].top_left; - marker_array.markers.push_back(marker); - } - - unique_id = 0; - for (std::size_t i = 0; i < constrain_ranges.size(); i++) { - visualization_msgs::msg::Marker constrain_range_text_marker; - constrain_range_text_marker.lifetime = rclcpp::Duration::from_seconds(0); - constrain_range_text_marker.header.frame_id = "map"; - constrain_range_text_marker.header.stamp = rclcpp::Time(0); - constrain_range_text_marker.ns = ns + "_location"; - constrain_range_text_marker.action = visualization_msgs::msg::Marker::ADD; - constrain_range_text_marker.pose.orientation.w = 1.0; - constrain_range_text_marker.id = unique_id; - constrain_range_text_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - constrain_range_text_marker.pose.position = constrain_ranges[i].top_left; - constrain_range_text_marker.scale = createMarkerScale(0, 0, 0.1); - constrain_range_text_marker.color = createMarkerColor(1.0, 0, 0, 0.99); - constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + - std::to_string(constrain_range_text_marker.pose.position.x) + - std::string("y ") + - std::to_string(constrain_range_text_marker.pose.position.y); - unique_id++; - marker_array.markers.push_back(constrain_range_text_marker); - - constrain_range_text_marker.id = unique_id; - constrain_range_text_marker.pose.position = constrain_ranges[i].top_right; - constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + - std::to_string(constrain_range_text_marker.pose.position.x) + - std::string("y ") + - std::to_string(constrain_range_text_marker.pose.position.y); - unique_id++; - marker_array.markers.push_back(constrain_range_text_marker); - - constrain_range_text_marker.id = unique_id; - constrain_range_text_marker.pose.position = constrain_ranges[i].bottom_left; - constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + - std::to_string(constrain_range_text_marker.pose.position.x) + - std::string("y ") + - std::to_string(constrain_range_text_marker.pose.position.y); - unique_id++; - marker_array.markers.push_back(constrain_range_text_marker); - - constrain_range_text_marker.id = unique_id; - constrain_range_text_marker.pose.position = constrain_ranges[i].bottom_right; - constrain_range_text_marker.text = std::to_string(i) + std::string(" x ") + - std::to_string(constrain_range_text_marker.pose.position.x) + - std::string("y ") + - std::to_string(constrain_range_text_marker.pose.position.y); - unique_id++; - marker_array.markers.push_back(constrain_range_text_marker); - } - return marker_array; -} - -visualization_msgs::msg::MarkerArray getObjectsMarkerArray( - const std::vector & objects, - const std::string & ns, const double r, const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int32_t i = 0; - for (const auto & object : objects) { - marker.id = i++; - marker.lifetime = rclcpp::Duration::from_seconds(1.0); - marker.type = visualization_msgs::msg::Marker::CUBE; - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose = object.kinematics.initial_pose_with_covariance.pose; - marker.scale = createMarkerScale(3.0, 1.0, 1.0); - marker.color = createMarkerColor(r, g, b, 0.8); - msg.markers.push_back(marker); - } - - return msg; -} - -visualization_msgs::msg::MarkerArray getRectanglesMarkerArray( - const std::vector & rects, const std::string & ns, const double r, - const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (const auto & rect : rects) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(1.0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::LINE_STRIP; - marker.scale = createMarkerScale(0.05, 0, 0); - marker.color = createMarkerColor(r, g, b, 0.025); - marker.points.push_back(rect.top_left); - marker.points.push_back(rect.top_right); - marker.points.push_back(rect.bottom_right); - marker.points.push_back(rect.bottom_left); - msg.markers.push_back(marker); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getRectanglesNumMarkerArray( - const std::vector & rects, const std::string & ns, const double r, - const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - int number_of_rect = 0; - for (const auto & rect : rects) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - marker.scale = createMarkerScale(0, 0, 0.125); - marker.color = createMarkerColor(r, g, b, 0.99); - marker.text = std::to_string(number_of_rect); - marker.pose.position = rect.top_left; - msg.markers.push_back(marker); - marker.id = unique_id++; - marker.pose.position = rect.top_right; - msg.markers.push_back(marker); - marker.id = unique_id++; - number_of_rect++; - } - return msg; -} - -visualization_msgs::msg::MarkerArray getPointsMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b) -{ - if (points.empty()) { - return visualization_msgs::msg::MarkerArray{}; - } - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - const int unique_id = 0; - marker.id = unique_id; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - marker.scale = createMarkerScale(0.5, 0.5, 0.5); - marker.color = createMarkerColor(r, g, b, 0.99); - for (const auto & p : points) { - marker.points.push_back(p.position); - } - msg.markers.push_back(marker); - return msg; -} - -visualization_msgs::msg::MarkerArray getPointsMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b) -{ - if (points.empty()) { - return visualization_msgs::msg::MarkerArray{}; - } - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - const int unique_id = 0; - marker.id = unique_id; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; - marker.scale = createMarkerScale(0.7, 0.7, 0.7); - marker.color = createMarkerColor(r, g, b, 0.99); - for (const auto & p : points) { - marker.points.push_back(p); - } - msg.markers.push_back(marker); - return msg; -} - -visualization_msgs::msg::MarkerArray getPointsTextMarkerArray( - const std::vector & points, const std::string & ns, const double r, - const double g, const double b) -{ - if (points.empty()) { - return visualization_msgs::msg::MarkerArray{}; - } - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (std::size_t i = 0; i < points.size(); i++) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - marker.scale = createMarkerScale(0, 0, 0.15); - marker.color = createMarkerColor(r, g, b, 0.99); - marker.text = std::to_string(i); - // marker.text = std::to_string(i) + " "+ std::to_string(points[i].position.x)+ " "+ - // std::to_string(points[i].position.y); - marker.pose.position = points[i].position; - msg.markers.push_back(marker); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getPointsTextMarkerArray( - const std::vector & points, - const std::string & ns, const double r, const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (std::size_t i = 0; i < points.size(); i++) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - marker.scale = createMarkerScale(0, 0, 0.15); - marker.color = createMarkerColor(r, g, b, 0.99); - // marker.text = std::to_string(i) + " "+ std::to_string(points[i].pose.position.x)+ " "+ - // std::to_string(points[i].pose.position.y); - marker.text = std::to_string(i); - marker.pose.position = points[i].pose.position; - msg.markers.push_back(marker); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getBaseBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_base, - const std::string & ns, const double r, const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (std::size_t i = 0; i < bounds.size(); i++) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::LINE_STRIP; - marker.scale = createMarkerScale(0.05, 0, 0); - marker.color = createMarkerColor(r, g, b, 0.6); - geometry_msgs::msg::Pose pose; - pose = candidate_base[i]; - geometry_msgs::msg::Point rel_lb; - rel_lb.x = 0; - rel_lb.y = bounds[i].c0.lb; - geometry_msgs::msg::Point abs_lb = util::transformToAbsoluteCoordinate2D(rel_lb, pose); - geometry_msgs::msg::Point rel_ub; - rel_ub.x = 0; - rel_ub.y = bounds[i].c0.ub; - geometry_msgs::msg::Point abs_ub = util::transformToAbsoluteCoordinate2D(rel_ub, pose); - marker.points.push_back(abs_lb); - marker.points.push_back(abs_ub); - msg.markers.push_back(marker); - marker.points.clear(); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getTopBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_top, - const std::string & ns, const double r, const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (std::size_t i = 0; i < bounds.size(); i++) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::LINE_STRIP; - marker.scale = createMarkerScale(0.05, 0, 0); - marker.color = createMarkerColor(r, g, b, 0.6); - geometry_msgs::msg::Point rel_lb; - rel_lb.x = 0; - rel_lb.y = bounds[i].c1.lb; - geometry_msgs::msg::Point abs_lb = - util::transformToAbsoluteCoordinate2D(rel_lb, candidate_top[i]); - geometry_msgs::msg::Point rel_ub; - rel_ub.x = 0; - rel_ub.y = bounds[i].c1.ub; - geometry_msgs::msg::Point abs_ub = - util::transformToAbsoluteCoordinate2D(rel_ub, candidate_top[i]); - marker.points.push_back(abs_lb); - marker.points.push_back(abs_ub); - msg.markers.push_back(marker); - marker.points.clear(); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getMidBoundsLineMarkerArray( - const std::vector & bounds, const std::vector & candidate_top, - const std::string & ns, const double r, const double g, const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker{}; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - - int unique_id = 0; - for (std::size_t i = 0; i < bounds.size(); i++) { - marker.id = unique_id++; - marker.lifetime = rclcpp::Duration::from_seconds(0); - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose.orientation.w = 1.0; - marker.type = visualization_msgs::msg::Marker::LINE_STRIP; - marker.scale = createMarkerScale(0.05, 0, 0); - marker.color = createMarkerColor(r, g, b, 0.6); - geometry_msgs::msg::Point rel_lb; - rel_lb.x = 0; - rel_lb.y = bounds[i].c2.lb; - geometry_msgs::msg::Point abs_lb = - util::transformToAbsoluteCoordinate2D(rel_lb, candidate_top[i]); - geometry_msgs::msg::Point rel_ub; - rel_ub.x = 0; - rel_ub.y = bounds[i].c2.ub; - geometry_msgs::msg::Point abs_ub = - util::transformToAbsoluteCoordinate2D(rel_ub, candidate_top[i]); - marker.points.push_back(abs_lb); - marker.points.push_back(abs_ub); - msg.markers.push_back(marker); - marker.points.clear(); - } - return msg; -} - -visualization_msgs::msg::MarkerArray getVirtualWallMarkerArray( - const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, - const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker; - marker.id = 0; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - marker.lifetime = rclcpp::Duration::from_seconds(1.0); - marker.type = visualization_msgs::msg::Marker::CUBE; - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose = pose; - marker.scale = createMarkerScale(0.1, 5.0, 2.0); - marker.color = createMarkerColor(r, g, b, 0.5); - msg.markers.push_back(marker); - return msg; -} - -visualization_msgs::msg::MarkerArray getVirtualWallTextMarkerArray( - const geometry_msgs::msg::Pose & pose, const std::string & ns, const double r, const double g, - const double b) -{ - const auto current_time = rclcpp::Clock().now(); - visualization_msgs::msg::MarkerArray msg; - - visualization_msgs::msg::Marker marker; - marker.id = 0; - marker.header.frame_id = "map"; - marker.header.stamp = current_time; - marker.ns = ns; - marker.lifetime = rclcpp::Duration::from_seconds(1.0); - marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - marker.action = visualization_msgs::msg::Marker::ADD; - marker.pose = pose; - marker.text = "drivable area"; - marker.scale = createMarkerScale(0.0, 0.0, 1.0); - marker.color = createMarkerColor(r, g, b, 0.99); - msg.markers.push_back(marker); - return msg; -} - -nav_msgs::msg::OccupancyGrid getDebugCostmap( - const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid) -{ - cv::Mat tmp; - clearance_map.copyTo(tmp); - cv::normalize(tmp, tmp, 0, 255, cv::NORM_MINMAX, CV_8UC1); - nav_msgs::msg::OccupancyGrid clearance_map_in_og = occupancy_grid; - tmp.forEach([&](const unsigned char & value, const int * position) -> void { - process_cv::putOccupancyGridValue(clearance_map_in_og, position[0], position[1], value); - }); - return clearance_map_in_og; -} diff --git a/planning/obstacle_avoidance_planner/src/debug_visualization.cpp b/planning/obstacle_avoidance_planner/src/debug_visualization.cpp index 86b3cc43420c0..2992ab4ccdffc 100644 --- a/planning/obstacle_avoidance_planner/src/debug_visualization.cpp +++ b/planning/obstacle_avoidance_planner/src/debug_visualization.cpp @@ -718,17 +718,7 @@ visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( const std::vector & optimized_points, const VehicleParam & vehicle_param, const bool is_showing_debug_detail) { - // virtual wall visualization_msgs::msg::MarkerArray vis_marker_array; - if (debug_data_ptr->stop_pose_by_drivable_area) { - const auto virtual_wall_pose = - getVirtualWallPose(debug_data_ptr->stop_pose_by_drivable_area.get(), vehicle_param); - appendMarkerArray( - getVirtualWallMarkerArray(virtual_wall_pose, "virtual_wall", 1.0, 0, 0), &vis_marker_array); - appendMarkerArray( - getVirtualWallTextMarkerArray(virtual_wall_pose, "virtual_wall_text", 1.0, 1.0, 1.0), - &vis_marker_array); - } if (is_showing_debug_detail) { const auto points_marker_array = getDebugPointsMarkers( @@ -809,6 +799,22 @@ visualization_msgs::msg::MarkerArray getDebugVisualizationMarker( return vis_marker_array; } +visualization_msgs::msg::MarkerArray getDebugVisualizationWallMarker( + const std::shared_ptr debug_data_ptr, const VehicleParam & vehicle_param) +{ + visualization_msgs::msg::MarkerArray vis_marker_array; + if (debug_data_ptr->stop_pose_by_drivable_area) { + const auto virtual_wall_pose = + getVirtualWallPose(debug_data_ptr->stop_pose_by_drivable_area.get(), vehicle_param); + appendMarkerArray( + getVirtualWallMarkerArray(virtual_wall_pose, "virtual_wall", 1.0, 0, 0), &vis_marker_array); + appendMarkerArray( + getVirtualWallTextMarkerArray(virtual_wall_pose, "virtual_wall_text", 1.0, 1.0, 1.0), + &vis_marker_array); + } + return vis_marker_array; +} + nav_msgs::msg::OccupancyGrid getDebugCostmap( const cv::Mat & clearance_map, const nav_msgs::msg::OccupancyGrid & occupancy_grid) { diff --git a/planning/obstacle_avoidance_planner/src/node.cpp b/planning/obstacle_avoidance_planner/src/node.cpp index 78abaf7ca50c5..a5caef472e665 100644 --- a/planning/obstacle_avoidance_planner/src/node.cpp +++ b/planning/obstacle_avoidance_planner/src/node.cpp @@ -207,6 +207,8 @@ ObstacleAvoidancePlanner::ObstacleAvoidancePlanner(const rclcpp::NodeOptions & n create_publisher("~/debug/mpt_traj", 1); debug_markers_pub_ = create_publisher("~/debug/marker", durable_qos); + debug_wall_markers_pub_ = + create_publisher("~/debug/wall_marker", durable_qos); debug_clearance_map_pub_ = create_publisher("~/debug/clearance_map", durable_qos); debug_object_clearance_map_pub_ = @@ -1161,6 +1163,19 @@ void ObstacleAvoidancePlanner::publishDebugDataInOptimization( debug_data_ptr_->msg_stream << " publishDebugVisualizationMarker:= " << stop_watch_.toc("publishDebugVisualizationMarker") << " [ms]\n"; + + stop_watch_.tic("getDebugVisualizationWallMarker"); + const auto & debug_wall_marker = + debug_visualization::getDebugVisualizationWallMarker(debug_data_ptr_, vehicle_param_); + debug_data_ptr_->msg_stream << " getDebugVisualizationWallMarker:= " + << stop_watch_.toc("getDebugVisualizationWallMarker") + << " [ms]\n"; + + stop_watch_.tic("publishDebugVisualizationWallMarker"); + debug_wall_markers_pub_->publish(debug_wall_marker); + debug_data_ptr_->msg_stream << " publishDebugVisualizationWallMarker:= " + << stop_watch_.toc("publishDebugVisualizationWallMarker") + << " [ms]\n"; } } From 3693cbb8f7514acd695ceb2c556224b8e67f62cf Mon Sep 17 00:00:00 2001 From: taikitanaka3 <65527974+taikitanaka3@users.noreply.github.com> Date: Fri, 18 Mar 2022 18:19:12 +0900 Subject: [PATCH 13/16] refactor(behavior_velocity): occlusion spot merge scene modules (#528) * refactor(behavior_velocity): refactor slice to general type Signed-off-by: tanaka3 * chore(behavior_velocity): Basic Polygon to general polygon2d Signed-off-by: tanaka3 * chore(behavior_velocity): generalize da polygon usage Signed-off-by: tanaka3 * chore(behavior_velocity): remove replaced function Signed-off-by: tanaka3 * refactor(behavior_velocity): unite create detection area polygon function as util Signed-off-by: tanaka3 * refactor(behavior_velocity): refactor method Signed-off-by: tanaka3 * refactor(behavior_velocity): add pass judge param Signed-off-by: tanaka3 * refactor(behavior_velocity): move judgement Signed-off-by: tanaka3 * refactor(behavior_velocity): replace with tier4 autoware utils Signed-off-by: tanaka3 * chore(behavior_velocity): fix spell check Signed-off-by: tanaka3 * refactor(behavior_velocity): merge occlusion spot sepated module Signed-off-by: tanaka3 * chore(behavior_velocity): handle boundary condition for better Signed-off-by: tanaka3 --- .../behavior_velocity_planner/CMakeLists.txt | 3 - .../config/occlusion_spot.param.yaml | 3 +- .../scene_module/occlusion_spot/geometry.hpp | 52 ------- .../occlusion_spot/grid_utils.hpp | 3 +- .../scene_module/occlusion_spot/manager.hpp | 2 +- .../occlusion_spot/occlusion_spot_utils.hpp | 56 ++++---- .../risk_predictive_braking.hpp | 9 -- .../scene_occlusion_spot_in_public_road.hpp | 73 ---------- .../include/utilization/util.hpp | 28 +++- .../src/scene_module/occlusion_spot/debug.cpp | 24 +--- .../scene_module/occlusion_spot/geometry.cpp | 133 ------------------ .../occlusion_spot/grid_utils.cpp | 4 +- .../scene_module/occlusion_spot/manager.cpp | 57 ++++---- .../occlusion_spot/occlusion_spot_utils.cpp | 78 ++++++---- .../risk_predictive_braking.cpp | 19 --- .../occlusion_spot/scene_occlusion_spot.cpp | 94 ++++++++----- .../scene_occlusion_spot_in_public_road.cpp | 104 -------------- .../src/utilization/util.cpp | 121 +++++++++++++++- .../test/src/test_geometry.cpp | 21 --- .../test/src/test_risk_predictive_braking.cpp | 43 ------ 20 files changed, 321 insertions(+), 606 deletions(-) delete mode 100644 planning/behavior_velocity_planner/include/scene_module/occlusion_spot/geometry.hpp delete mode 100644 planning/behavior_velocity_planner/include/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.hpp delete mode 100644 planning/behavior_velocity_planner/src/scene_module/occlusion_spot/geometry.cpp delete mode 100644 planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.cpp delete mode 100644 planning/behavior_velocity_planner/test/src/test_geometry.cpp diff --git a/planning/behavior_velocity_planner/CMakeLists.txt b/planning/behavior_velocity_planner/CMakeLists.txt index 927793f713433..824f18706fcfe 100644 --- a/planning/behavior_velocity_planner/CMakeLists.txt +++ b/planning/behavior_velocity_planner/CMakeLists.txt @@ -220,10 +220,8 @@ target_link_libraries(scene_module_virtual_traffic_light scene_module_lib) # SceneModule OcclusionSpot ament_auto_add_library(scene_module_occlusion_spot SHARED src/scene_module/occlusion_spot/grid_utils.cpp - src/scene_module/occlusion_spot/geometry.cpp src/scene_module/occlusion_spot/manager.cpp src/scene_module/occlusion_spot/debug.cpp - src/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.cpp src/scene_module/occlusion_spot/scene_occlusion_spot.cpp src/scene_module/occlusion_spot/occlusion_spot_utils.cpp src/scene_module/occlusion_spot/risk_predictive_braking.cpp @@ -313,7 +311,6 @@ if(BUILD_TESTING) ament_add_gtest(occlusion_spot-test test/src/test_occlusion_spot_utils.cpp test/src/test_risk_predictive_braking.cpp - test/src/test_geometry.cpp test/src/test_grid_utils.cpp ) target_link_libraries(occlusion_spot-test diff --git a/planning/behavior_velocity_planner/config/occlusion_spot.param.yaml b/planning/behavior_velocity_planner/config/occlusion_spot.param.yaml index 5784cce657e0d..71bcf2694fa0d 100644 --- a/planning/behavior_velocity_planner/config/occlusion_spot.param.yaml +++ b/planning/behavior_velocity_planner/config/occlusion_spot.param.yaml @@ -1,7 +1,8 @@ /**: ros__parameters: occlusion_spot: - method: "predicted_object" # [-] candidate is "occupancy_grid" or "predicted_object" + detection_method: "predicted_object" # [-] candidate is "occupancy_grid" or "predicted_object" + pass_judge: "current_velocity" # [-] candidate is "current_velocity"" debug: false # [-] whether to publish debug markers. Note Default should be false for performance use_partition_lanelet: true # [-] whether to use partition lanelet map data pedestrian_vel: 1.0 # [m/s] assume pedestrian is dashing from occlusion at this velocity diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/geometry.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/geometry.hpp deleted file mode 100644 index e022df839bccb..0000000000000 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/geometry.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 Tier IV, Inc. -// -// 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 SCENE_MODULE__OCCLUSION_SPOT__GEOMETRY_HPP_ -#define SCENE_MODULE__OCCLUSION_SPOT__GEOMETRY_HPP_ - -#include - -#include - -#include -#include - -#include - -namespace behavior_velocity_planner -{ -namespace occlusion_spot_utils -{ -namespace bg = boost::geometry; - -//!< @brief build slices all along the trajectory -// using the given range and desired slice length and width -void buildSlices( - std::vector & slices, const lanelet::ConstLanelet & path_lanelet, const double offset, - const bool is_on_right, const PlannerParam & param); -//!< @brief build detection_area slice from path -void buildDetectionAreaPolygon( - std::vector & slices, const PathWithLaneId & path, const double offset, - const PlannerParam & param); -//!< @brief calculate interpolation between a and b at distance ratio t -template -T lerp(T a, T b, double t) -{ - return a + t * (b - a); -} - -} // namespace occlusion_spot_utils -} // namespace behavior_velocity_planner - -#endif // SCENE_MODULE__OCCLUSION_SPOT__GEOMETRY_HPP_ diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/grid_utils.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/grid_utils.hpp index 8d7edf060cf6f..b7f9df3371b66 100644 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/grid_utils.hpp +++ b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/grid_utils.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -67,7 +68,7 @@ bool isOcclusionSpotSquare( //!< @brief Find all occlusion spots inside the given lanelet void findOcclusionSpots( std::vector & occlusion_spot_positions, const grid_map::GridMap & grid, - const lanelet::BasicPolygon2d & polygon, const double min_size); + const Polygon2d & polygon, const double min_size); //!< @brief Return true if the path between the two given points is free of occupied cells bool isCollisionFree( const grid_map::GridMap & grid, const grid_map::Position & p1, const grid_map::Position & p2); diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/manager.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/manager.hpp index 6464226c788b0..831dca20f1655 100644 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/manager.hpp +++ b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/manager.hpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -49,6 +48,7 @@ class OcclusionSpotModuleManager : public SceneModuleManagerInterface using PlannerParam = occlusion_spot_utils::PlannerParam; PlannerParam planner_param_; + int64_t module_id_; void launchNewModules(const autoware_auto_planning_msgs::msg::PathWithLaneId & path) override; diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/occlusion_spot_utils.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/occlusion_spot_utils.hpp index 65547fd0d763a..0146947be2039 100644 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/occlusion_spot_utils.hpp +++ b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/occlusion_spot_utils.hpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include @@ -42,6 +42,16 @@ #include #include +namespace tier4_autoware_utils +{ +template <> +inline geometry_msgs::msg::Pose getPose( + const autoware_auto_planning_msgs::msg::PathPointWithLaneId & p) +{ + return p.point.pose; +} +} // namespace tier4_autoware_utils + namespace behavior_velocity_planner { using autoware_auto_perception_msgs::msg::ObjectClassification; @@ -67,7 +77,8 @@ using BasicPolygons2d = std::vector; namespace occlusion_spot_utils { enum ROAD_TYPE { PRIVATE, PUBLIC, HIGHWAY, UNKNOWN }; -enum METHOD { OCCUPANCY_GRID, PREDICTED_OBJECT }; +enum DETECTION_METHOD { OCCUPANCY_GRID, PREDICTED_OBJECT }; +enum PASS_JUDGE { SMOOTH_VELOCITY, CURRENT_VELOCITY }; struct DetectionArea { @@ -99,7 +110,8 @@ struct LatLon struct PlannerParam { - METHOD method; + DETECTION_METHOD detection_method; + PASS_JUDGE pass_judge; bool debug; // [-] bool use_partition_lanelet; // [-] // parameters in yaml @@ -127,22 +139,6 @@ struct SafeMotion double safe_velocity; }; -// @brief represent the range of a each polygon -struct SliceRange -{ - double min_length{}; - double max_length{}; - double min_distance{}; - double max_distance{}; -}; - -// @brief representation of a polygon along a path -struct Slice -{ - SliceRange range{}; - lanelet::BasicPolygon2d polygon{}; -}; - struct ObstacleInfo { SafeMotion safe_motion; // safe motion of velocity and stop point @@ -185,7 +181,7 @@ struct DebugData double z; std::string road_type = ""; std::string detection_type = ""; - std::vector detection_area_polygons; + Polygons2d detection_area_polygons; std::vector partition_lanelets; std::vector parked_vehicle_point; std::vector possible_collisions; @@ -200,22 +196,26 @@ struct DebugData occlusion_points.clear(); } }; - +// apply current velocity to path +PathWithLaneId applyVelocityToPath(const PathWithLaneId & path, const double v0); +//!< @brief wrapper for detection area polygon generation +bool buildDetectionAreaPolygon( + Polygons2d & slices, const PathWithLaneId & path, const double offset, + const PlannerParam & param); lanelet::ConstLanelet toPathLanelet(const PathWithLaneId & path); // Note : consider offset_from_start_to_ego and safety margin for collision here void handleCollisionOffset(std::vector & possible_collisions, double offset); void clipPathByLength( const PathWithLaneId & path, PathWithLaneId & clipped, const double max_length = 100.0); bool isStuckVehicle(PredictedObject obj, const double min_vel); -double offsetFromStartToEgo( - const PathWithLaneId & path, const Pose & ego_pose, const int closest_idx); std::vector filterDynamicObjectByDetectionArea( - std::vector & objs, const std::vector & polys); + std::vector & objs, const Polygons2d & polys); std::vector getParkedVehicles( const PredictedObjects & dyn_objects, const PlannerParam & param, std::vector & debug_point); -std::vector generatePossibleCollisionBehindParkedVehicle( - const PathWithLaneId & path, const PlannerParam & param, const double offset_from_start_to_ego, +bool generatePossibleCollisionBehindParkedVehicle( + std::vector & possible_collisions, const PathWithLaneId & path, + const PlannerParam & param, const double offset_from_start_to_ego, const std::vector & dyn_objects); ROAD_TYPE getCurrentRoadType( const lanelet::ConstLanelet & current_lanelet, const LaneletMapPtr & lanelet_map_ptr); @@ -236,10 +236,10 @@ void calcSlowDownPointsForPossibleCollision( //!< @brief convert a set of occlusion spots found on detection_area slice boost::optional generateOneNotableCollisionFromOcclusionSpot( const grid_map::GridMap & grid, const std::vector & occlusion_spot_positions, - const double offset_from_start_to_ego, const BasicPoint2d base_point, + const double offset_from_start_to_ego, const Point2d base_point, const lanelet::ConstLanelet & path_lanelet, const PlannerParam & param, DebugData & debug_data); //!< @brief generate possible collisions coming from occlusion spots on the side of the path -void createPossibleCollisionsInDetectionArea( +bool createPossibleCollisionsInDetectionArea( std::vector & possible_collisions, const grid_map::GridMap & grid, const PathWithLaneId & path, const double offset_from_start_to_ego, const PlannerParam & param, DebugData & debug_data); diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/risk_predictive_braking.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/risk_predictive_braking.hpp index 9156d5d04ce07..3be1bf9c5f0fd 100644 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/risk_predictive_braking.hpp +++ b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/risk_predictive_braking.hpp @@ -34,15 +34,6 @@ int insertSafeVelocityToPath( const geometry_msgs::msg::Pose & in_pose, const double safe_vel, const PlannerParam & param, PathWithLaneId * inout_path); -/** - * - * @param: longitudinal_distance: longitudinal distance to collision - * @param: param: planner param - * @return lateral distance - **/ -double calculateLateralDistanceFromTTC( - const double longitudinal_distance, const PlannerParam & param); - /** * @param: v: ego velocity config * @param: ttc: time to collision diff --git a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.hpp b/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.hpp deleted file mode 100644 index b194a4fb83d91..0000000000000 --- a/planning/behavior_velocity_planner/include/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.hpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2021 Tier IV, Inc. -// -// 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 SCENE_MODULE__OCCLUSION_SPOT__SCENE_OCCLUSION_SPOT_IN_PUBLIC_ROAD_HPP_ -#define SCENE_MODULE__OCCLUSION_SPOT__SCENE_OCCLUSION_SPOT_IN_PUBLIC_ROAD_HPP_ - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include - -#include -#include -#include - -namespace behavior_velocity_planner -{ -class OcclusionSpotInPublicModule : public SceneModuleInterface -{ - using PlannerParam = occlusion_spot_utils::PlannerParam; - using DebugData = occlusion_spot_utils::DebugData; - -public: - OcclusionSpotInPublicModule( - const int64_t module_id, std::shared_ptr planner_data, - const PlannerParam & planner_param, const rclcpp::Logger logger, - const rclcpp::Clock::SharedPtr clock); - - /** - * @brief plan occlusion spot velocity - */ - bool modifyPathVelocity( - autoware_auto_planning_msgs::msg::PathWithLaneId * path, - tier4_planning_msgs::msg::StopReason * stop_reason) override; - - visualization_msgs::msg::MarkerArray createDebugMarkerArray() override; - -private: - autoware_auto_perception_msgs::msg::PredictedObjects::ConstSharedPtr predicted_objects_array_; - - // Parameter - PlannerParam param_; - -protected: - int64_t module_id_; - - // Debug - mutable DebugData debug_data_; -}; -} // namespace behavior_velocity_planner - -#endif // SCENE_MODULE__OCCLUSION_SPOT__SCENE_OCCLUSION_SPOT_IN_PUBLIC_ROAD_HPP_ diff --git a/planning/behavior_velocity_planner/include/utilization/util.hpp b/planning/behavior_velocity_planner/include/utilization/util.hpp index cd7b029cf1651..6632127a9e98e 100644 --- a/planning/behavior_velocity_planner/include/utilization/util.hpp +++ b/planning/behavior_velocity_planner/include/utilization/util.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -63,18 +64,31 @@ struct SearchRangeIndex size_t min_idx; size_t max_idx; }; +struct DetectionRange +{ + bool use_right = true; + bool use_left = true; + double interval; + double min_longitudinal_distance; + double max_longitudinal_distance; + double min_lateral_distance; + double max_lateral_distance; +}; struct PointWithSearchRangeIndex { geometry_msgs::msg::Point point; SearchRangeIndex index; }; -using autoware_auto_planning_msgs::msg::PathWithLaneId; + using Point2d = boost::geometry::model::d2::point_xy; -using Polygons2d = std::vector; +using autoware_auto_planning_msgs::msg::PathPoint; using autoware_auto_planning_msgs::msg::PathPointWithLaneId; using autoware_auto_planning_msgs::msg::PathWithLaneId; +using BasicPolygons2d = std::vector; +using Polygons2d = std::vector; namespace planning_utils { +using geometry_msgs::msg::Pose; inline geometry_msgs::msg::Point getPoint(const geometry_msgs::msg::Point & p) { return p; } inline geometry_msgs::msg::Point getPoint(const geometry_msgs::msg::Pose & p) { return p.position; } inline geometry_msgs::msg::Point getPoint(const geometry_msgs::msg::PoseStamped & p) @@ -110,7 +124,15 @@ inline geometry_msgs::msg::Pose getPose( { return traj.points.at(idx).pose; } -void getAllPartitionLanelets(const lanelet::LaneletMapConstPtr ll, Polygons2d & polys); + +// create detection area from given range return false if creation failure +bool createDetectionAreaPolygons( + Polygons2d & slices, const PathWithLaneId & path, const DetectionRange da_range, + const double obstacle_vel_mps); +PathPoint getLerpPathPointWithLaneId(const PathPoint p0, const PathPoint p1, const double ratio); +Point2d calculateLateralOffsetPoint2d(const Pose & p, const double offset); + +void getAllPartitionLanelets(const lanelet::LaneletMapConstPtr ll, BasicPolygons2d & polys); void setVelocityFrom(const size_t idx, const double vel, PathWithLaneId * input); void insertVelocity( PathWithLaneId & path, const PathPointWithLaneId & path_point, const double v, diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/debug.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/debug.cpp index cb435400f1feb..b0749792956fd 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/debug.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/debug.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -29,7 +28,6 @@ namespace { using builtin_interfaces::msg::Time; using BasicPolygons = std::vector; -using Slices = std::vector; visualization_msgs::msg::Marker makeArrowMarker( const occlusion_spot_utils::PossibleCollisionInfo & possible_collision, const int id) @@ -173,7 +171,7 @@ visualization_msgs::msg::MarkerArray makePolygonMarker( } visualization_msgs::msg::MarkerArray makeSlicePolygonMarker( - const Slices & slices, const std::string ns, const int id, const double z) + const Polygons2d & slices, const std::string ns, const int id, const double z) { visualization_msgs::msg::MarkerArray debug_markers; visualization_msgs::msg::Marker debug_marker; @@ -189,7 +187,7 @@ visualization_msgs::msg::MarkerArray makeSlicePolygonMarker( debug_marker.lifetime = rclcpp::Duration::from_seconds(0.1); debug_marker.ns = ns; for (const auto & slice : slices) { - for (const auto & p : slice.polygon) { + for (const auto & p : slice.outer()) { geometry_msgs::msg::Point point = tier4_autoware_utils::createMarkerPosition(p.x(), p.y(), 0.0); debug_marker.points.push_back(point); @@ -283,24 +281,6 @@ visualization_msgs::msg::MarkerArray createOcclusionMarkerArray( } } // namespace -visualization_msgs::msg::MarkerArray OcclusionSpotInPublicModule::createDebugMarkerArray() -{ - const auto current_time = this->clock_->now(); - - visualization_msgs::msg::MarkerArray debug_marker_array; - if (!debug_data_.possible_collisions.empty()) { - appendMarkerArray( - createPossibleCollisionMarkers(debug_data_, module_id_), current_time, &debug_marker_array); - } - if (!debug_data_.detection_area_polygons.empty()) { - appendMarkerArray( - makeSlicePolygonMarker( - debug_data_.detection_area_polygons, "detection_area", module_id_, debug_data_.z), - current_time, &debug_marker_array); - } - - return debug_marker_array; -} visualization_msgs::msg::MarkerArray OcclusionSpotModule::createDebugMarkerArray() { const auto current_time = this->clock_->now(); diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/geometry.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/geometry.cpp deleted file mode 100644 index acb1827bc686b..0000000000000 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/geometry.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2021 Tier IV, Inc. -// -// 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 -#include -#include -#include -#include -#include - -#include -#include - -namespace behavior_velocity_planner -{ -namespace occlusion_spot_utils -{ -using lanelet::BasicLineString2d; -using lanelet::BasicPoint2d; -using lanelet::BasicPolygon2d; -namespace bg = boost::geometry; -namespace lg = lanelet::geometry; - -BasicPoint2d calculateLateralOffsetPoint( - const BasicPoint2d & p0, const BasicPoint2d & p1, const double offset) -{ - // translation - const double dy = p1[1] - p0[1]; - const double dx = p1[0] - p0[0]; - // rotation (use inverse matrix of rotation) - const double yaw = std::atan2(dy, dx); - const double offset_x = p1[0] - std::sin(yaw) * offset; - const double offset_y = p1[1] + std::cos(yaw) * offset; - return BasicPoint2d{offset_x, offset_y}; -} - -void buildSlices( - std::vector & slices, const lanelet::ConstLanelet & path_lanelet, const double offset, - const bool is_on_right, const PlannerParam & param) -{ - BasicLineString2d center_line = path_lanelet.centerline2d().basicLineString(); - const auto & p = param; - /** - * @brief relationships for interpolated polygon - * - * +(min_length,max_distance)-+ - +---+(max_length,max_distance) = outer_polygons - * | | - * +--------------------------+ - +---+(max_length,min_distance) = inner_polygons - */ - const double min_length = offset; // + p.baselink_to_front; - // Note: min_detection_area_length is for occlusion spot visualization but not effective for - // planning - const double min_detection_area_length = 10.0; - const double max_length = std::max( - min_detection_area_length, std::min(p.detection_area_max_length, p.detection_area_length)); - const double min_distance = (is_on_right) ? -p.half_vehicle_width : p.half_vehicle_width; - const double slice_length = p.detection_area.slice_length; - const int num_step = static_cast(slice_length); - //! max index is the last index of path point - const int max_index = static_cast(center_line.size() - 2); - int idx = 0; - /** - * Note: create polygon from path point is better than from ego offset to consider below - * - less computation cost and no need to recalculated interpolated point start from ego - * - less stable for localization noise - **/ - for (int s = 0; s < max_index; s += num_step) { - const double length = s * slice_length; - const double next_length = static_cast(s + num_step); - /// if (max_length < length) continue; - Slice slice; - BasicLineString2d inner_polygons; - BasicLineString2d outer_polygons; - // build connected polygon for lateral - for (int i = 0; i <= num_step; i++) { - idx = s + i; - const double arc_length_from_ego = std::max(0.0, static_cast(idx - min_length)); - if (arc_length_from_ego > max_length) break; - if (idx >= max_index) break; - const auto & c0 = center_line.at(idx); - const auto & c1 = center_line.at(idx + 1); - /** - * @brief points - * +--outer point (lateral distance obstacle can reach) - * | - * +--inner point(min distance) - */ - const BasicPoint2d inner_point = calculateLateralOffsetPoint(c0, c1, min_distance); - double lateral_distance = calculateLateralDistanceFromTTC(arc_length_from_ego, param); - if (is_on_right) lateral_distance *= -1; - const BasicPoint2d outer_point = calculateLateralOffsetPoint(c0, c1, lateral_distance); - inner_polygons.emplace_back(inner_point); - outer_polygons.emplace_back(outer_point); - } - if (inner_polygons.empty()) continue; - // connect invert point - inner_polygons.insert(inner_polygons.end(), outer_polygons.rbegin(), outer_polygons.rend()); - slice.polygon = lanelet::BasicPolygon2d(inner_polygons); - // add range info - slice.range.min_length = length; - slice.range.max_length = next_length; - slices.emplace_back(slice); - } -} - -void buildDetectionAreaPolygon( - std::vector & slices, const PathWithLaneId & path, const double offset, - const PlannerParam & param) -{ - std::vector left_slices; - std::vector right_slices; - lanelet::ConstLanelet path_lanelet = toPathLanelet(path); - // in most case lateral distance is much more effective for velocity planning - buildSlices(left_slices, path_lanelet, offset, false /*is_on_right*/, param); - buildSlices(right_slices, path_lanelet, offset, true /*is_on_right*/, param); - // Properly order lanelets from closest to furthest - slices = left_slices; - slices.insert(slices.end(), right_slices.begin(), right_slices.end()); - return; -} -} // namespace occlusion_spot_utils -} // namespace behavior_velocity_planner diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/grid_utils.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/grid_utils.cpp index 8e8b408082e78..8e91a5735b981 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/grid_utils.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/grid_utils.cpp @@ -80,12 +80,12 @@ bool isOcclusionSpotSquare( void findOcclusionSpots( std::vector & occlusion_spot_positions, const grid_map::GridMap & grid, - const lanelet::BasicPolygon2d & polygon, double min_size) + const Polygon2d & polygon, double min_size) { const grid_map::Matrix & grid_data = grid["layer"]; const int min_occlusion_spot_size = std::max(0.0, std::floor(min_size / grid.getResolution())); grid_map::Polygon grid_polygon; - for (const auto & point : polygon) { + for (const auto & point : polygon.outer()) { grid_polygon.addVertex({point.x(), point.y()}); } for (grid_map::PolygonIterator iterator(grid, grid_polygon); !iterator.isPastEnd(); ++iterator) { diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/manager.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/manager.cpp index 44b30e7814e51..25ac665744861 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/manager.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/manager.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include @@ -25,7 +24,8 @@ namespace behavior_velocity_planner { -using occlusion_spot_utils::METHOD; +using occlusion_spot_utils::DETECTION_METHOD; +using occlusion_spot_utils::PASS_JUDGE; OcclusionSpotModuleManager::OcclusionSpotModuleManager(rclcpp::Node & node) : SceneModuleManagerInterface(node, getModuleName()) @@ -36,14 +36,31 @@ OcclusionSpotModuleManager::OcclusionSpotModuleManager(rclcpp::Node & node) // for crosswalk parameters auto & pp = planner_param_; - // assume pedestrian coming out from occlusion spot with this velocity - const std::string method = node.declare_parameter(ns + ".method", "occupancy_grid"); - if (method == "occupancy_grid") { - pp.method = METHOD::OCCUPANCY_GRID; - } else if (method == "predicted_object") { - pp.method = METHOD::PREDICTED_OBJECT; - } else { - throw std::invalid_argument{"[behavior_velocity]: occlusion spot detection method is invalid"}; + // for detection type + { + const std::string method = node.declare_parameter(ns + ".detection_method", "occupancy_grid"); + if (method == "occupancy_grid") { // module id 0 + pp.detection_method = DETECTION_METHOD::OCCUPANCY_GRID; + module_id_ = DETECTION_METHOD::OCCUPANCY_GRID; + } else if (method == "predicted_object") { // module id 1 + pp.detection_method = DETECTION_METHOD::PREDICTED_OBJECT; + module_id_ = DETECTION_METHOD::PREDICTED_OBJECT; + } else { + throw std::invalid_argument{ + "[behavior_velocity]: occlusion spot detection method has invalid argument"}; + } + } + // for passable judgement + { + const std::string pass_judge = node.declare_parameter(ns + ".pass_judge", "current_velocity"); + if (pass_judge == "current_velocity") { + pp.pass_judge = PASS_JUDGE::CURRENT_VELOCITY; + } else if (pass_judge == "smooth_velocity") { + pp.pass_judge = PASS_JUDGE::SMOOTH_VELOCITY; + } else { + throw std::invalid_argument{ + "[behavior_velocity]: occlusion spot pass judge method has invalid argument"}; + } } pp.debug = node.declare_parameter(ns + ".debug", false); pp.use_partition_lanelet = node.declare_parameter(ns + ".use_partition_lanelet", false); @@ -84,23 +101,11 @@ void OcclusionSpotModuleManager::launchNewModules( const autoware_auto_planning_msgs::msg::PathWithLaneId & path) { if (path.points.empty()) return; - const int64_t module_id = static_cast(ModuleID::OCCUPANCY); - const int64_t public_road_module_id = static_cast(ModuleID::OBJECT); // general - if (!isModuleRegistered(module_id)) { - if (planner_param_.method == METHOD::OCCUPANCY_GRID) { - registerModule(std::make_shared( - module_id, planner_data_, planner_param_, logger_.get_child("occlusion_spot_module"), - clock_, pub_debug_occupancy_grid_)); - } - } - // public - if (!isModuleRegistered(public_road_module_id)) { - if (planner_param_.method == METHOD::PREDICTED_OBJECT) { - registerModule(std::make_shared( - public_road_module_id, planner_data_, planner_param_, - logger_.get_child("occlusion_spot_in_public_module"), clock_)); - } + if (!isModuleRegistered(module_id_)) { + registerModule(std::make_shared( + module_id_, planner_data_, planner_param_, logger_.get_child("occlusion_spot_module"), clock_, + pub_debug_occupancy_grid_)); } } diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/occlusion_spot_utils.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/occlusion_spot_utils.cpp index 87d351e0635fb..c0586aabff665 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/occlusion_spot_utils.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/occlusion_spot_utils.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -50,6 +49,32 @@ lanelet::ConstLanelet toPathLanelet(const PathWithLaneId & path) return lanelet::ConstLanelet(path_lanelet); } +PathWithLaneId applyVelocityToPath(const PathWithLaneId & path, const double v0) +{ + PathWithLaneId out; + for (size_t i = 0; i < path.points.size(); i++) { + PathPointWithLaneId p = path.points.at(i); + p.point.longitudinal_velocity_mps = std::max(v0, 1.0); + out.points.emplace_back(p); + } + return out; +} + +bool buildDetectionAreaPolygon( + Polygons2d & slices, const PathWithLaneId & path, const double offset, const PlannerParam & param) +{ + const auto & p = param; + DetectionRange da_range; + da_range.interval = p.detection_area.slice_length; + da_range.min_longitudinal_distance = offset + p.baselink_to_front; + da_range.max_longitudinal_distance = + std::min(p.detection_area_max_length, p.detection_area_length) + + da_range.min_longitudinal_distance; + da_range.min_lateral_distance = p.half_vehicle_width; + da_range.max_lateral_distance = p.detection_area.max_lateral_distance; + return planning_utils::createDetectionAreaPolygons(slices, path, da_range, p.pedestrian_vel); +} + ROAD_TYPE getCurrentRoadType( const lanelet::ConstLanelet & current_lanelet, [[maybe_unused]] const lanelet::LaneletMapPtr & lanelet_map_ptr) @@ -178,21 +203,6 @@ bool isStuckVehicle(PredictedObject obj, const double min_vel) return false; } -double offsetFromStartToEgo( - const PathWithLaneId & path, const Pose & ego_pose, const int closest_idx) -{ - double offset_from_ego_to_closest = 0; - for (int i = 0; i < closest_idx; i++) { - const auto & curr_p = path.points.at(i).point.pose.position; - const auto & next_p = path.points.at(i + 1).point.pose.position; - offset_from_ego_to_closest += tier4_autoware_utils::calcDistance2d(curr_p, next_p); - } - const double offset_from_closest_to_target = - -planning_utils::transformRelCoordinate2D(ego_pose, path.points[closest_idx].point.pose) - .position.x; - return offset_from_ego_to_closest + offset_from_closest_to_target; -} - std::vector getParkedVehicles( const PredictedObjects & dyn_objects, const PlannerParam & param, std::vector & debug_point) @@ -303,12 +313,12 @@ PossibleCollisionInfo calculateCollisionPathPointFromOcclusionSpot( return pc; } -std::vector generatePossibleCollisionBehindParkedVehicle( - const PathWithLaneId & path, const PlannerParam & param, const double offset_from_start_to_ego, +bool generatePossibleCollisionBehindParkedVehicle( + std::vector & possible_collisions, const PathWithLaneId & path, + const PlannerParam & param, const double offset_from_start_to_ego, const std::vector & dyn_objects) { lanelet::ConstLanelet path_lanelet = toPathLanelet(path); - std::vector possible_collisions; auto ll = path_lanelet.centerline2d(); for (const auto & dyn : dyn_objects) { ArcCoordinates arc_coord_occlusion = getOcclusionPoint(dyn, ll); @@ -334,11 +344,12 @@ std::vector generatePossibleCollisionBehindParkedVehicle( [](PossibleCollisionInfo pc1, PossibleCollisionInfo pc2) { return pc1.arc_lane_dist_at_collision.length < pc2.arc_lane_dist_at_collision.length; }); - return possible_collisions; + if (possible_collisions.empty()) return false; + return true; } std::vector filterDynamicObjectByDetectionArea( - std::vector & objs, const std::vector & polys) + std::vector & objs, const Polygons2d & polys) { std::vector filtered_obj; // stuck points by predicted objects @@ -346,7 +357,7 @@ std::vector filterDynamicObjectByDetectionArea( // check if the footprint is in the stuck detect area const Polygon2d obj_footprint = planning_utils::toFootprintPolygon(object); for (const auto & p : polys) { - if (!bg::disjoint(obj_footprint, p.polygon)) { + if (!bg::disjoint(obj_footprint, p)) { filtered_obj.emplace_back(object); } } @@ -354,20 +365,21 @@ std::vector filterDynamicObjectByDetectionArea( return filtered_obj; } -void createPossibleCollisionsInDetectionArea( +bool createPossibleCollisionsInDetectionArea( std::vector & possible_collisions, const grid_map::GridMap & grid, const PathWithLaneId & path, const double offset_from_start_to_ego, const PlannerParam & param, DebugData & debug_data) { lanelet::ConstLanelet path_lanelet = toPathLanelet(path); if (path_lanelet.centerline2d().empty()) { - return; + return true; } double distance_lower_bound = std::numeric_limits::max(); - for (const Slice & detection_area_slice : debug_data.detection_area_polygons) { + const Polygons2d & da_polygons = debug_data.detection_area_polygons; + for (const Polygon2d & detection_area_slice : da_polygons) { std::vector occlusion_spot_positions; grid_utils::findOcclusionSpots( - occlusion_spot_positions, grid, detection_area_slice.polygon, + occlusion_spot_positions, grid, detection_area_slice, param.detection_area.min_occlusion_spot_size); if (param.debug) { for (const auto & op : occlusion_spot_positions) { @@ -378,7 +390,7 @@ void createPossibleCollisionsInDetectionArea( } if (occlusion_spot_positions.empty()) continue; // for each partition find nearest occlusion spot from polygon's origin - BasicPoint2d base_point = detection_area_slice.polygon.at(0); + const Point2d base_point = detection_area_slice.outer().at(0); const auto pc = generateOneNotableCollisionFromOcclusionSpot( grid, occlusion_spot_positions, offset_from_start_to_ego, base_point, path_lanelet, param, debug_data); @@ -388,6 +400,14 @@ void createPossibleCollisionsInDetectionArea( distance_lower_bound = lateral_distance; possible_collisions.emplace_back(pc.get()); } + // sort by arc length + std::sort( + possible_collisions.begin(), possible_collisions.end(), + [](PossibleCollisionInfo pc1, PossibleCollisionInfo pc2) { + return pc1.arc_lane_dist_at_collision.length < pc2.arc_lane_dist_at_collision.length; + }); + if (possible_collisions.empty()) return false; + return true; } bool isNotBlockedByPartition(const LineString2d & direction, const BasicPolygons2d & partitions) @@ -400,7 +420,7 @@ bool isNotBlockedByPartition(const LineString2d & direction, const BasicPolygons boost::optional generateOneNotableCollisionFromOcclusionSpot( const grid_map::GridMap & grid, const std::vector & occlusion_spot_positions, - const double offset_from_start_to_ego, const BasicPoint2d base_point, + const double offset_from_start_to_ego, const Point2d base_point, const lanelet::ConstLanelet & path_lanelet, const PlannerParam & param, DebugData & debug_data) { const double baselink_to_front = param.baselink_to_front; @@ -414,7 +434,7 @@ boost::optional generateOneNotableCollisionFromOcclusionS const lanelet::BasicPoint2d obstacle_point = { occlusion_spot_position[0], occlusion_spot_position[1]}; const double dist = - std::hypot(base_point[0] - obstacle_point[0], base_point[1] - obstacle_point[1]); + std::hypot(base_point.x() - obstacle_point[0], base_point.y() - obstacle_point[1]); // skip if absolute distance is larger if (distance_lower_bound < dist) continue; lanelet::ArcCoordinates arc_coord_occlusion_point = diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/risk_predictive_braking.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/risk_predictive_braking.cpp index 94150d0a43fcd..38250d6d78255 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/risk_predictive_braking.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/risk_predictive_braking.cpp @@ -98,25 +98,6 @@ int insertSafeVelocityToPath( return 0; } -double calculateLateralDistanceFromTTC( - const double longitudinal_distance, const PlannerParam & param) -{ - const auto & v = param.v; - const auto & p = param; - double v_min = 1.0; - const double lateral_buffer = 0.5; - const double min_distance = p.half_vehicle_width + lateral_buffer; - const double max_distance = p.detection_area.max_lateral_distance; - if (longitudinal_distance <= 0) return min_distance; - if (v_min < param.v.min_allowed_velocity) v_min = param.v.min_allowed_velocity; - // use min velocity if ego velocity is below min allowed - const double v0 = (v.v_ego > v_min) ? v.v_ego : v_min; - // here is a part where ego t(ttc) can be replaced by calculation of velocity smoother or ? - double t = longitudinal_distance / v0; - double lateral_distance = t * param.pedestrian_vel + p.half_vehicle_width; - return std::min(max_distance, std::max(min_distance, lateral_distance)); -} - SafeMotion calculateSafeMotion(const Velocity & v, const double ttc) { SafeMotion sm; diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot.cpp index 726c5e7bec799..9cd7f1851247f 100644 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot.cpp +++ b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -22,6 +21,8 @@ #include #include +#include + #include #include @@ -29,8 +30,6 @@ namespace behavior_velocity_planner { -namespace bg = boost::geometry; -namespace lg = lanelet::geometry; namespace utils = occlusion_spot_utils; OcclusionSpotModule::OcclusionSpotModule( @@ -38,10 +37,15 @@ OcclusionSpotModule::OcclusionSpotModule( const PlannerParam & planner_param, const rclcpp::Logger logger, const rclcpp::Clock::SharedPtr clock, const rclcpp::Publisher::SharedPtr publisher) -: SceneModuleInterface(module_id, logger, clock), publisher_(publisher) +: SceneModuleInterface(module_id, logger, clock), publisher_(publisher), param_(planner_param) { - param_ = planner_param; - debug_data_.detection_type = "occupancy"; + if (param_.detection_method == utils::DETECTION_METHOD::OCCUPANCY_GRID) { + debug_data_.detection_type = "occupancy"; + //! occupancy grid limitation( 100 * 100 ) + param_.detection_area_length = std::min(50.0, param_.detection_area_length); + } else if (param_.detection_method == utils::DETECTION_METHOD::PREDICTED_OBJECT) { + debug_data_.detection_type = "object"; + } if (param_.use_partition_lanelet) { const lanelet::LaneletMapConstPtr & ll = planner_data->route_handler_->getLaneletMapPtr(); planning_utils::getAllPartitionLanelets(ll, debug_data_.partition_lanelets); @@ -62,41 +66,63 @@ bool OcclusionSpotModule::modifyPathVelocity( param_.v.max_stop_accel = planner_data_->max_stop_acceleration_threshold; param_.v.v_ego = planner_data_->current_velocity->twist.linear.x; param_.v.a_ego = planner_data_->current_accel.get(); - param_.detection_area_max_length = planning_utils::calcJudgeLineDistWithJerkLimit( - param_.v.v_ego, param_.v.a_ego, param_.v.non_effective_accel, param_.v.non_effective_jerk, - 0.0); + const double detection_area_offset = 5.0; // for visualization and stability + param_.detection_area_max_length = + planning_utils::calcJudgeLineDistWithJerkLimit( + param_.v.v_ego, param_.v.a_ego, param_.v.non_effective_accel, param_.v.non_effective_jerk, + planner_data_->delay_response_time) + + detection_area_offset; } const geometry_msgs::msg::Pose ego_pose = planner_data_->current_pose.pose; - const auto & occ_grid_ptr = planner_data_->occupancy_grid; - if (!occ_grid_ptr) { - return true; - } - - const double max_range = 50.0; PathWithLaneId clipped_path; - utils::clipPathByLength(*path, clipped_path, max_range); + utils::clipPathByLength(*path, clipped_path, param_.detection_area_length); PathWithLaneId interp_path; //! never change this interpolation interval(will affect module accuracy) splineInterpolate(clipped_path, 1.0, &interp_path, logger_); - debug_data_.interp_path = interp_path; - int closest_idx = -1; - if (!planning_utils::calcClosestIndex( - interp_path, ego_pose, closest_idx, param_.dist_thr, param_.angle_thr)) { - return true; + if (param_.pass_judge == utils::PASS_JUDGE::CURRENT_VELOCITY) { + interp_path = utils::applyVelocityToPath(interp_path, param_.v.v_ego); + } else if (param_.pass_judge == utils::PASS_JUDGE::SMOOTH_VELOCITY) { } - // return if ego is final point of interpolated path - if (closest_idx == static_cast(interp_path.points.size()) - 1) return true; - nav_msgs::msg::OccupancyGrid occ_grid = *occ_grid_ptr; - grid_map::GridMap grid_map; - grid_utils::denoiseOccupancyGridCV(occ_grid, grid_map, param_.grid); - double offset_from_start_to_ego = utils::offsetFromStartToEgo(interp_path, ego_pose, closest_idx); + debug_data_.interp_path = interp_path; + const geometry_msgs::msg::Point start_point = interp_path.points.at(0).point.pose.position; + const auto offset = tier4_autoware_utils::calcSignedArcLength( + interp_path.points, ego_pose, start_point, param_.dist_thr, param_.angle_thr); + if (offset == boost::none) return true; + const double offset_from_start_to_ego = -offset.get(); auto & detection_area_polygons = debug_data_.detection_area_polygons; - utils::buildDetectionAreaPolygon( - detection_area_polygons, interp_path, offset_from_start_to_ego, param_); + if (!utils::buildDetectionAreaPolygon( + detection_area_polygons, interp_path, offset_from_start_to_ego, param_)) { + return true; // path point is not enough + } std::vector possible_collisions; - // Note: Don't consider offset from path start to ego here - utils::createPossibleCollisionsInDetectionArea( - possible_collisions, grid_map, interp_path, offset_from_start_to_ego, param_, debug_data_); + if (param_.detection_method == utils::DETECTION_METHOD::OCCUPANCY_GRID) { + const auto & occ_grid_ptr = planner_data_->occupancy_grid; + if (!occ_grid_ptr) return true; // mo data + nav_msgs::msg::OccupancyGrid occ_grid = *occ_grid_ptr; + grid_map::GridMap grid_map; + grid_utils::denoiseOccupancyGridCV(occ_grid, grid_map, param_.grid); + if (param_.debug) publisher_->publish(occ_grid); // + // Note: Don't consider offset from path start to ego here + if (!utils::createPossibleCollisionsInDetectionArea( + possible_collisions, grid_map, interp_path, offset_from_start_to_ego, param_, + debug_data_)) { + // no occlusion spot + return true; + } + } else if (param_.detection_method == utils::DETECTION_METHOD::PREDICTED_OBJECT) { + const auto & dynamic_obj_arr_ptr = planner_data_->predicted_objects; + if (!dynamic_obj_arr_ptr) return true; // mo data + std::vector obj = + utils::getParkedVehicles(*dynamic_obj_arr_ptr, param_, debug_data_.parked_vehicle_point); + const auto filtered_obj = + utils::filterDynamicObjectByDetectionArea(obj, detection_area_polygons); + // Note: Don't consider offset from path start to ego here + if (!utils::generatePossibleCollisionBehindParkedVehicle( + possible_collisions, interp_path, param_, offset_from_start_to_ego, filtered_obj)) { + // no occlusion spot + return true; + } + } RCLCPP_DEBUG_STREAM_THROTTLE( logger_, *clock_, 3000, "num possible collision:" << possible_collisions.size()); utils::calcSlowDownPointsForPossibleCollision(0, interp_path, 0.0, possible_collisions); @@ -105,9 +131,7 @@ bool OcclusionSpotModule::modifyPathVelocity( // apply safe velocity using ebs and pbs deceleration utils::applySafeVelocityConsideringPossibleCollision(path, possible_collisions, param_); // these debug topics needs computation resource - if (param_.debug) { - publisher_->publish(occ_grid); - } + debug_data_.z = path->points.front().point.pose.position.z; debug_data_.possible_collisions = possible_collisions; debug_data_.path_raw = *path; diff --git a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.cpp b/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.cpp deleted file mode 100644 index 7040c7c221af1..0000000000000 --- a/planning/behavior_velocity_planner/src/scene_module/occlusion_spot/scene_occlusion_spot_in_public_road.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2021 Tier IV, Inc. -// -// 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 -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include - -namespace behavior_velocity_planner -{ -using occlusion_spot_utils::PossibleCollisionInfo; -namespace utils = occlusion_spot_utils; - -OcclusionSpotInPublicModule::OcclusionSpotInPublicModule( - const int64_t module_id, [[maybe_unused]] std::shared_ptr planner_data, - const PlannerParam & planner_param, const rclcpp::Logger logger, - const rclcpp::Clock::SharedPtr clock) -: SceneModuleInterface(module_id, logger, clock) -{ - param_ = planner_param; - debug_data_.detection_type = "object"; - if (param_.use_partition_lanelet) { - const lanelet::LaneletMapConstPtr & ll = planner_data->route_handler_->getLaneletMapPtr(); - planning_utils::getAllPartitionLanelets(ll, debug_data_.partition_lanelets); - } -} - -bool OcclusionSpotInPublicModule::modifyPathVelocity( - autoware_auto_planning_msgs::msg::PathWithLaneId * path, - [[maybe_unused]] tier4_planning_msgs::msg::StopReason * stop_reason) -{ - debug_data_.resetData(); - if (path->points.size() < 2) { - return true; - } - // set planner data - { - param_.v.max_stop_jerk = planner_data_->max_stop_jerk_threshold; - param_.v.max_stop_accel = planner_data_->max_stop_acceleration_threshold; - param_.v.v_ego = planner_data_->current_velocity->twist.linear.x; - param_.v.a_ego = planner_data_->current_accel.get(); - param_.detection_area_max_length = planning_utils::calcJudgeLineDistWithJerkLimit( - param_.v.v_ego, param_.v.a_ego, param_.v.non_effective_accel, param_.v.non_effective_jerk, - 0.0); - } - const geometry_msgs::msg::Pose ego_pose = planner_data_->current_pose.pose; - const auto & dynamic_obj_arr_ptr = planner_data_->predicted_objects; - - if (!dynamic_obj_arr_ptr) { - return true; - } - PathWithLaneId clipped_path; - utils::clipPathByLength(*path, clipped_path, param_.detection_area_length); - PathWithLaneId interp_path; - splineInterpolate(clipped_path, 1.0, &interp_path, logger_); - int closest_idx = -1; - if (!planning_utils::calcClosestIndex( - interp_path, ego_pose, closest_idx, param_.dist_thr, param_.angle_thr)) { - return true; - } - // return if ego is final point of interpolated path - if (closest_idx == static_cast(interp_path.points.size()) - 1) return true; - std::vector obj = - utils::getParkedVehicles(*dynamic_obj_arr_ptr, param_, debug_data_.parked_vehicle_point); - double offset_from_start_to_ego = utils::offsetFromStartToEgo(interp_path, ego_pose, closest_idx); - auto & detection_area_polygons = debug_data_.detection_area_polygons; - utils::buildDetectionAreaPolygon( - detection_area_polygons, interp_path, offset_from_start_to_ego, param_); - const auto filtered_obj = utils::filterDynamicObjectByDetectionArea(obj, detection_area_polygons); - // Note: Don't consider offset from path start to ego here - std::vector possible_collisions = - utils::generatePossibleCollisionBehindParkedVehicle( - interp_path, param_, offset_from_start_to_ego, filtered_obj); - utils::calcSlowDownPointsForPossibleCollision(0, interp_path, 0.0, possible_collisions); - // Note: Consider offset from path start to ego here - utils::handleCollisionOffset(possible_collisions, offset_from_start_to_ego); - // apply safe velocity using ebs and pbs deceleration - utils::applySafeVelocityConsideringPossibleCollision(path, possible_collisions, param_); - debug_data_.possible_collisions = possible_collisions; - return true; -} - -} // namespace behavior_velocity_planner diff --git a/planning/behavior_velocity_planner/src/utilization/util.cpp b/planning/behavior_velocity_planner/src/utilization/util.cpp index 57906fe36c01f..5e1cf7e377654 100644 --- a/planning/behavior_velocity_planner/src/utilization/util.cpp +++ b/planning/behavior_velocity_planner/src/utilization/util.cpp @@ -23,7 +23,126 @@ namespace behavior_velocity_planner { namespace planning_utils { -void getAllPartitionLanelets(const lanelet::LaneletMapConstPtr ll, Polygons2d & polys) +Point2d calculateLateralOffsetPoint2d(const Pose & pose, const double offset) +{ + using tier4_autoware_utils::calcOffsetPose; + return to_bg2d(calcOffsetPose(pose, 0.0, offset, 0.0)); +} + +PathPoint getLerpPathPointWithLaneId(const PathPoint p0, const PathPoint p1, const double ratio) +{ + auto lerp = [](const double a, const double b, const double t) { return a + t * (b - a); }; + PathPoint p; + Pose pose; + const auto pp0 = p0.pose.position; + const auto pp1 = p1.pose.position; + pose.position.x = lerp(pp0.x, pp1.x, ratio); + pose.position.y = lerp(pp0.y, pp1.y, ratio); + pose.position.z = lerp(pp0.z, pp1.z, ratio); + const double yaw = tier4_autoware_utils::calcAzimuthAngle(pp0, pp1); + pose.orientation = tier4_autoware_utils::createQuaternionFromYaw(yaw); + p.pose = pose; + const double v = lerp(p0.longitudinal_velocity_mps, p1.longitudinal_velocity_mps, ratio); + p.longitudinal_velocity_mps = v; + return p; +} + +bool createDetectionAreaPolygons( + Polygons2d & da_polys, const PathWithLaneId & path, const DetectionRange da_range, + const double obstacle_vel_mps) +{ + /** + * @brief relationships for interpolated polygon + * + * +(min_length,max_distance)-+ - +---+(max_length,max_distance) = outer_polygons + * | | + * +--------------------------+ - +---+(max_length,min_distance) = inner_polygons + */ + const double min_len = da_range.min_longitudinal_distance; + const double max_len = da_range.max_longitudinal_distance; + const double min_dst = da_range.min_lateral_distance; + const double max_dst = da_range.max_lateral_distance; + const double interval = da_range.interval; + const double min_velocity = 0.5; // min velocity that autoware can cruise stably + //! max index is the last index of path point + const size_t max_index = static_cast(path.points.size() - 1); + double dist_sum = 0; + size_t first_idx = 0; // first path point found in front of ego front bumper + offset + const auto & pp = path.points; + //! avoid bug with same point polygon + const double eps = 1e-3; + if (path.points.size() < 2) return false; // case of path point is only one + auto p0 = path.points.front().point; + // handle the first point + { + double current_dist = 0.0; + for (size_t i = 1; i <= max_index - 1; i++) { + dist_sum += tier4_autoware_utils::calcDistance2d(pp.at(i - 1), pp.at(i)); + if (dist_sum > min_len) { + first_idx = i; + break; + } + current_dist = dist_sum; + } + if (first_idx == 0) return false; // case of all path point is behind ego front bumper + offset + const double ds = dist_sum - current_dist; + if (std::abs(ds) < eps) { + p0 = pp.at(first_idx - 1).point; + } else { + const double ratio = (min_len - current_dist) / ds; + p0 = getLerpPathPointWithLaneId(pp.at(first_idx - 1).point, pp.at(first_idx).point, ratio); + } + } + double ttc = 0.0; + dist_sum = min_len; + double length = 0; + // initial point of detection area polygon + LineString2d left_inner_bound = {calculateLateralOffsetPoint2d(p0.pose, min_dst)}; + LineString2d left_outer_bound = {calculateLateralOffsetPoint2d(p0.pose, min_dst + eps)}; + LineString2d right_inner_bound = {calculateLateralOffsetPoint2d(p0.pose, -min_dst)}; + LineString2d right_outer_bound = {calculateLateralOffsetPoint2d(p0.pose, -min_dst - eps)}; + for (size_t s = first_idx; s <= max_index; s++) { + const auto p1 = path.points.at(s).point; + const double ds = tier4_autoware_utils::calcDistance2d(p0, p1); + dist_sum += ds; + length += ds; + // calculate the distance that obstacles can move until ego reach the trajectory point + const double v_average = 0.5 * (p0.longitudinal_velocity_mps + p1.longitudinal_velocity_mps); + const double v = std::max(v_average, min_velocity); + const double dt = ds / v; + ttc += dt; + // for offset calculation + const double max_lateral_distance = std::min(max_dst, min_dst + ttc * obstacle_vel_mps + eps); + // left bound + if (da_range.use_left) { + left_inner_bound.emplace_back(calculateLateralOffsetPoint2d(p1.pose, min_dst)); + left_outer_bound.emplace_back(calculateLateralOffsetPoint2d(p1.pose, max_lateral_distance)); + } + // right bound + if (da_range.use_right) { + right_inner_bound.emplace_back(calculateLateralOffsetPoint2d(p1.pose, -min_dst)); + right_outer_bound.emplace_back(calculateLateralOffsetPoint2d(p1.pose, -max_lateral_distance)); + } + // replace previous point with next point + p0 = p1; + // separate detection area polygon with fixed interval or at the end of detection max length + if (length > interval || max_len < dist_sum || s == max_index) { + if (left_inner_bound.size() > 1) + da_polys.emplace_back(lines2polygon(left_inner_bound, left_outer_bound)); + if (right_inner_bound.size() > 1) + da_polys.emplace_back(lines2polygon(right_outer_bound, right_inner_bound)); + left_inner_bound = {left_inner_bound.back()}; + left_outer_bound = {left_outer_bound.back()}; + right_inner_bound = {right_inner_bound.back()}; + right_outer_bound = {right_outer_bound.back()}; + length = 0; + if (max_len < dist_sum || s == max_index) return true; + } + } + return true; +} + +void getAllPartitionLanelets(const lanelet::LaneletMapConstPtr ll, BasicPolygons2d & polys) { const lanelet::ConstLineStrings3d partitions = lanelet::utils::query::getAllPartitions(ll); for (const auto & partition : partitions) { diff --git a/planning/behavior_velocity_planner/test/src/test_geometry.cpp b/planning/behavior_velocity_planner/test/src/test_geometry.cpp deleted file mode 100644 index 0d7abf4bf7271..0000000000000 --- a/planning/behavior_velocity_planner/test/src/test_geometry.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2021 Tier IV, Inc. -// -// 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 "utils.hpp" - -#include - -#include - -#include diff --git a/planning/behavior_velocity_planner/test/src/test_risk_predictive_braking.cpp b/planning/behavior_velocity_planner/test/src/test_risk_predictive_braking.cpp index ff47f2414428a..7238dd971b02e 100644 --- a/planning/behavior_velocity_planner/test/src/test_risk_predictive_braking.cpp +++ b/planning/behavior_velocity_planner/test/src/test_risk_predictive_braking.cpp @@ -65,49 +65,6 @@ TEST(safeMotion, delay_jerk_acceleration) } } -TEST(detectionArea, calcLateralDistance) -{ - namespace utils = behavior_velocity_planner::occlusion_spot_utils; - using utils::calculateLateralDistanceFromTTC; - /** - * @brief check if calculation is correct in below parameter - * lateral distance is calculated from - * - ego velocity - * - min distance(safety margin) - * - max distance(ignore distance above this) - * - pedestrian velocity - * - min allowed velocity(not to stop) - */ - utils::PlannerParam p; - p.half_vehicle_width = 2.0; - p.baselink_to_front = 3.0; - p.pedestrian_vel = 1.5; - p.detection_area.max_lateral_distance = 5.0; - p.v.min_allowed_velocity = 1.5; - p.v.v_ego = 5.0; - const double offset_from_ego_to_start = 0.0; - { - for (size_t i = 0; i <= 15; i += 5) { - // arc length in path point - const double l = i * 1.0; - const double s = l - offset_from_ego_to_start; - const double d = utils::calculateLateralDistanceFromTTC(s, p); - const double eps = 1e-3; - std::cout << "s: " << l << " v: " << p.v.v_ego << " d: " << d << std::endl; - if (i == 0) - EXPECT_NEAR(d, 2.5, eps); - else if (i == 5) - EXPECT_NEAR(d, 3.5, eps); - else if (i == 10) - EXPECT_NEAR(d, 5.0, eps); - else if (i == 15) - EXPECT_NEAR(d, 5.0, eps); - else - break; - } - } -} - TEST(calculateInsertVelocity, min_max) { using behavior_velocity_planner::occlusion_spot_utils::calculateInsertVelocity; From 0c6643eb60c23a964ce086d62c88205cd0783ca3 Mon Sep 17 00:00:00 2001 From: Yutaka Shimizu <43805014+purewater0901@users.noreply.github.com> Date: Fri, 18 Mar 2022 23:04:49 +0900 Subject: [PATCH 14/16] fix(map_based_prediction): use autoware utils (#547) * use autoware utils * add const --- .../map_based_prediction/src/map_based_prediction_node.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/perception/map_based_prediction/src/map_based_prediction_node.cpp b/perception/map_based_prediction/src/map_based_prediction_node.cpp index 39722cc0eddf7..d2d95fb5e3eac 100644 --- a/perception/map_based_prediction/src/map_based_prediction_node.cpp +++ b/perception/map_based_prediction/src/map_based_prediction_node.cpp @@ -301,7 +301,7 @@ double MapBasedPredictionNode::getObjectYaw(const TrackedObject & object) const auto & object_twist = object.kinematics.twist_with_covariance.twist; const auto future_object_pose = tier4_autoware_utils::calcOffsetPose( object_pose, object_twist.linear.x * 0.1, object_twist.linear.y * 0.1, 0.0); - return tier4_autoware_utils::calcAzimuthAngle(future_object_pose.position, object_pose.position); + return tier4_autoware_utils::calcAzimuthAngle(object_pose.position, future_object_pose.position); } void MapBasedPredictionNode::removeOldObjectsHistory(const double current_time) @@ -412,11 +412,11 @@ bool MapBasedPredictionNode::checkCloseLaneletCondition( } // Step3. Calculate the angle difference between the lane angle and obstacle angle - double object_yaw = getObjectYaw(object); + const double object_yaw = getObjectYaw(object); const double lane_yaw = lanelet::utils::getLaneletAngle( lanelet.second, object.kinematics.pose_with_covariance.pose.position); const double delta_yaw = object_yaw - lane_yaw; - const double normalized_delta_yaw = std::atan2(std::sin(delta_yaw), std::cos(delta_yaw)); + const double normalized_delta_yaw = tier4_autoware_utils::normalizeRadian(delta_yaw); const double abs_norm_delta = std::fabs(normalized_delta_yaw); // Step4. Check if the closest lanelet is valid, and add all From 939a5db7e6ae2abc7d51efda8bf39c6566b0db03 Mon Sep 17 00:00:00 2001 From: Yukihiro Saito Date: Sat, 19 Mar 2022 16:04:38 +0900 Subject: [PATCH 15/16] feat: detected object validation (#507) --- .../detected_object_validation/CMakeLists.txt | 64 +++++ .../detected_object_validation/README.md | 50 ++++ .../image/debug_image.png | Bin 0 -> 528076 bytes .../occupancy_grid_based_validator.hpp | 74 +++++ .../occupancy_grid_based_validator.launch.xml | 31 +++ .../detected_object_validation/package.xml | 29 ++ .../src/occupancy_grid_based_validator.cpp | 258 ++++++++++++++++++ perception/detection_by_tracker/package.xml | 1 + .../laserscan_to_occupancy_grid_map_node.cpp | 3 +- 9 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 perception/detected_object_validation/CMakeLists.txt create mode 100644 perception/detected_object_validation/README.md create mode 100644 perception/detected_object_validation/image/debug_image.png create mode 100644 perception/detected_object_validation/include/occupancy_grid_based_validator/occupancy_grid_based_validator.hpp create mode 100644 perception/detected_object_validation/launch/occupancy_grid_based_validator.launch.xml create mode 100644 perception/detected_object_validation/package.xml create mode 100644 perception/detected_object_validation/src/occupancy_grid_based_validator.cpp diff --git a/perception/detected_object_validation/CMakeLists.txt b/perception/detected_object_validation/CMakeLists.txt new file mode 100644 index 0000000000000..546b7442fb12b --- /dev/null +++ b/perception/detected_object_validation/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.5) +project(detected_object_validation) + +### Compile options +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic -Werror) +endif() + +# Ignore -Wnonportable-include-path in Clang for mussp +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wno-nonportable-include-path) +endif() + +### Find Packages +find_package(ament_cmake_auto REQUIRED) + +### Find OpenCV Dependencies +find_package(OpenCV REQUIRED) + +### Find Eigen Dependencies +find_package(eigen3_cmake_module REQUIRED) +find_package(Eigen3 REQUIRED) + +### Find dependencies listed in the package.xml +ament_auto_find_build_dependencies() + +include_directories( + include + SYSTEM + ${OpenCV_INCLUDE_DIRS} +) + +# Generate exe file +set(OCCUPANCY_GRID_BASED_VALIDATOR_SRC + src/occupancy_grid_based_validator.cpp +) + +ament_auto_add_library(occupancy_grid_based_validator SHARED + ${OCCUPANCY_GRID_BASED_VALIDATOR_SRC} +) + +target_link_libraries(occupancy_grid_based_validator + ${OpenCV_LIBRARIES} + Eigen3::Eigen +) + +rclcpp_components_register_node(occupancy_grid_based_validator + PLUGIN "occupancy_grid_based_validator::OccupancyGridBasedValidator" + EXECUTABLE occupancy_grid_based_validator_node +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_auto_package(INSTALL_TO_SHARE + launch +) diff --git a/perception/detected_object_validation/README.md b/perception/detected_object_validation/README.md new file mode 100644 index 0000000000000..2bb1f9d3a4e8d --- /dev/null +++ b/perception/detected_object_validation/README.md @@ -0,0 +1,50 @@ +# detected_object_validation (occupancy grid based validator) + +## Purpose + +The purpose of this package is to eliminate obvious false positives of DetectedObjects. + +## Inner-workings / Algorithms + +Compare the occupancy grid map with the DetectedObject, and if a larger percentage of obstacles are in freespace, delete them. + +![debug sample image](image/debug_image.png) + +Basically, it takes an occupancy grid map as input and generates a binary image of freespace or other. + +A mask image is generated for each DetectedObject and the average value (percentage) in the mask image is calculated. +If the percentage is low, it is deleted. + +## Inputs / Outputs + +### Input + +| Name | Type | Description | +| ---------------------------- | ----------------------------------------------------- | ----------------------------------------------------------- | +| `~/input/detected_objects` | `autoware_auto_perception_msgs::msg::DetectedObjects` | DetectedObjects | +| `~/input/occupancy_grid_map` | `nav_msgs::msg::OccupancyGrid` | OccupancyGrid with no time series calculation is preferred. | + +### Output + +| Name | Type | Description | +| ------------------ | ----------------------------------------------------- | ------------------------- | +| `~/output/objects` | `autoware_auto_perception_msgs::msg::DetectedObjects` | validated DetectedObjects | + +## Parameters + +| Name | Type | Description | +| ---------------- | ----- | -------------------------------------------------- | +| `mean_threshold` | float | The percentage threshold of allowed non-freespace. | +| `enable_debug` | bool | Whether to display debug images or not? | + +## Assumptions / Known limits + +Currently, only vehicle represented as BoundingBox are supported. + +## (Optional) Error detection and handling + +## (Optional) Performance characterization + +## (Optional) References/External links + +## (Optional) Future extensions / Unimplemented parts diff --git a/perception/detected_object_validation/image/debug_image.png b/perception/detected_object_validation/image/debug_image.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac379a243d17e34edadef97361d219e38c1b064 GIT binary patch literal 528076 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