simple_planner v1.4.0
Loading...
Searching...
No Matches
object_handling.cpp
Go to the documentation of this file.
1// Copyright Institute for Automotive Engineering (ika), RWTH Aachen University
2// SPDX-License-Identifier: Apache-2.0
3
4#include <algorithm>
5#include <array>
6#include <cmath>
7#include <optional>
8#include <string>
9#include <vector>
10
11#include <Eigen/Dense>
12
14#include <tf2_perception_msgs/tf2_perception_msgs.hpp>
15
16// Object-handling part of SimplePlannerNode: turning the perceived object list into a speed cap
17// on the route-following trajectory, plus the RViz visualization of the explaining conflict.
18// The pure geometry helpers used here live in simple_planner/object_geometry.hpp.
19
20namespace simple_planner {
21
22void SimplePlannerNode::resetObjectState(const std_msgs::msg::Header& target_header) {
25 clearObjectInteractionMarkers(target_header);
26}
27
28void SimplePlannerNode::clearObjectInteractionMarkers(const std_msgs::msg::Header& target_header) {
30 return;
31 }
32
33 visualization_msgs::msg::MarkerArray marker_array;
34 const std::array<std::string, 3> namespaces = {"object_interaction_ego_safety_box", "object_interaction_ego_box",
35 "object_interaction_object_box"};
36 int marker_id = 0;
37 for (const auto& marker_namespace : namespaces) {
38 visualization_msgs::msg::Marker marker;
39 marker.header = target_header;
40 marker.ns = marker_namespace;
41 marker.id = marker_id++;
42 marker.action = visualization_msgs::msg::Marker::DELETE;
43 marker_array.markers.push_back(marker);
44 }
45 object_interaction_marker_pub_->publish(marker_array);
46}
47
48void SimplePlannerNode::publishObjectInteractionMarkers(const std_msgs::msg::Header& target_header,
49 const std::optional<ConflictSample>& conflict) {
51 return;
52 }
53
54 if (!publish_object_interaction_markers_ || !conflict.has_value()) {
55 clearObjectInteractionMarkers(target_header);
56 return;
57 }
58
59 visualization_msgs::msg::MarkerArray marker_array;
60 auto make_box_marker = [&](const std::string& ns, int marker_id, const geometry_msgs::msg::Pose& pose, double length,
61 double width, double z, float r, float g, float b, float a) {
62 visualization_msgs::msg::Marker marker;
63 marker.header = target_header;
64 marker.ns = ns;
65 marker.id = marker_id;
66 marker.type = visualization_msgs::msg::Marker::CUBE;
67 marker.action = visualization_msgs::msg::Marker::ADD;
68 marker.pose = pose;
69 marker.pose.position.z = z;
70 marker.scale.x = length;
71 marker.scale.y = width;
72 marker.scale.z = 0.08;
73 marker.lifetime.sec = 0;
74 marker.lifetime.nanosec = 500000000;
75 marker.color.r = r;
76 marker.color.g = g;
77 marker.color.b = b;
78 marker.color.a = a;
79 marker_array.markers.push_back(marker);
80 };
81
82 const geometry_msgs::msg::Pose ego_pose = toPose(conflict->ego_box);
83 const OrientedBox2D ego_safety_box =
85 const double ego_length = 2.0 * conflict->ego_box.half_length;
86 const double ego_width = 2.0 * conflict->ego_box.half_width;
87 make_box_marker("object_interaction_ego_safety_box", 0, toPose(ego_safety_box), 2.0 * ego_safety_box.half_length,
88 2.0 * ego_safety_box.half_width, 0.12, 1.0F, 0.55F, 0.0F, 0.28F);
89 make_box_marker("object_interaction_ego_box", 1, ego_pose, ego_length, ego_width, 0.18, 1.0F, 0.0F, 0.0F, 0.55F);
90 make_box_marker("object_interaction_object_box", 2, toPose(conflict->object_box), 2.0 * conflict->object_box.half_length,
91 2.0 * conflict->object_box.half_width, 0.24, 0.0F, 0.45F, 1.0F, 0.55F);
92
93 object_interaction_marker_pub_->publish(marker_array);
94}
95
96std::vector<ObjectTrajectory> SimplePlannerNode::buildObjectTrajectories(const perception_msgs::msg::ObjectList& tf_object_list,
97 const rclcpp::Time& stamp) const {
98 std::vector<ObjectTrajectory> object_trajectories;
99 for (const auto& object : tf_object_list.objects) {
100 double object_width = 0.0;
101 double object_length = 0.0;
102 try {
103 object_width = perception_msgs::object_access::getWidth(object);
104 object_length = perception_msgs::object_access::getLength(object);
105 } catch (const std::exception&) {
106 object_width = 0.0;
107 object_length = 0.0;
108 }
109 if (!std::isfinite(object_width) || object_width < kMinObjectWidth) object_width = kMinObjectWidth;
110 if (!std::isfinite(object_length) || object_length < kMinObjectLength) object_length = kMinObjectLength;
111
112 auto add_static_object = [&]() {
113 ObjectTrajectory trajectory;
114 trajectory.id = object.id;
115 trajectory.is_static = true;
116 trajectory.samples.push_back(buildObjectSample(object.state, tf_object_list.header, stamp, object_length, object_width));
117 object_trajectories.push_back(trajectory);
118 };
119
120 // Select all sufficiently likely predictions, or fall back to the single most likely one.
121 std::vector<const perception_msgs::msg::ObjectStatePrediction*> selected_predictions;
122 for (const auto& prediction : object.state_predictions) {
123 if (prediction.probability >= min_prediction_prob_) selected_predictions.push_back(&prediction);
124 }
125 if (selected_predictions.empty() && !object.state_predictions.empty()) {
126 selected_predictions.push_back(
127 &*std::max_element(object.state_predictions.begin(), object.state_predictions.end(),
128 [](const auto& lhs, const auto& rhs) { return lhs.probability < rhs.probability; }));
129 }
130
131 if (selected_predictions.empty()) {
132 add_static_object();
133 continue;
134 }
135
136 for (const auto* prediction : selected_predictions) {
137 if (prediction == nullptr || prediction->states.empty()) {
138 add_static_object();
139 continue;
140 }
141 ObjectTrajectory trajectory;
142 trajectory.id = object.id;
143 trajectory.is_static = false;
144 trajectory.samples.reserve(prediction->states.size());
145 for (const auto& state : prediction->states) {
146 trajectory.samples.push_back(buildObjectSample(state, tf_object_list.header, stamp, object_length, object_width));
147 }
148 object_trajectories.push_back(trajectory);
149 }
150 }
151 return object_trajectories;
152}
153
154std::optional<ConflictSample> SimplePlannerNode::firstConflict(const std::vector<SimplePathPoint>& ego_path,
155 const std::vector<ObjectTrajectory>& object_trajectories) const {
156 if (ego_path.empty()) return std::nullopt;
157
158 const auto& ego_offset_msg = ego_data_.state.reference_point.translation_to_geometric_center;
159 const Eigen::Vector2d ego_center_offset(ego_offset_msg.x, ego_offset_msg.y);
160
161 // Interpolates the ego bounding box along the (time-equidistant) path at relative time ego_t.
162 auto ego_box_at = [&](double ego_t) {
163 const size_t idx = std::min(static_cast<size_t>(std::floor(std::max(ego_t, 0.0) / dt_)), ego_path.size() - 1);
164 const size_t next_idx = std::min(idx + 1, ego_path.size() - 1);
165 const double segment_start_t = dt_ * static_cast<double>(idx);
166 const double alpha = next_idx > idx ? std::clamp((ego_t - segment_start_t) / dt_, 0.0, 1.0) : 0.0;
167 const Eigen::Vector2d position = ego_path[idx].position + alpha * (ego_path[next_idx].position - ego_path[idx].position);
168 Eigen::Vector2d heading = ego_path[next_idx].position - ego_path[idx].position;
169 if (heading.squaredNorm() < 1e-9 && idx > 0) heading = ego_path[idx].position - ego_path[idx - 1].position;
170 const double yaw = heading.squaredNorm() > 1e-9 ? wrap_angle_rad(std::atan2(heading.y(), heading.x())) : 0.0;
171 return buildOrientedBox(position + rotate(ego_center_offset, yaw), yaw, ego_data_.length, ego_data_.width);
172 };
173
174 const double check_dt = std::clamp(kObjectCollisionCheckDt, 0.01, dt_);
175 const size_t last_segment_idx = ego_path.size() > 1 ? ego_path.size() - 2 : 0;
176 for (size_t i = 0; i <= last_segment_idx; ++i) {
177 const double segment_start_t = dt_ * static_cast<double>(i);
178 if (segment_start_t > trajectory_horizon_) break;
179 const double segment_end_t = std::min(dt_ * static_cast<double>(i + 1), trajectory_horizon_);
180 const int steps = std::max(1, static_cast<int>(std::ceil((segment_end_t - segment_start_t) / check_dt)));
181
182 for (int step = 0; step <= steps; ++step) {
183 const double ego_t =
184 segment_start_t + (segment_end_t - segment_start_t) * static_cast<double>(step) / static_cast<double>(steps);
185 const OrientedBox2D ego_box = ego_box_at(ego_t);
186 const OrientedBox2D ego_safety_box =
188
189 for (const auto& object_trajectory : object_trajectories) {
190 if (object_trajectory.samples.empty()) continue;
191
192 if (object_trajectory.is_static) {
193 const auto& object_sample = object_trajectory.samples.front();
194 if (overlaps(ego_safety_box, object_sample.box)) {
195 return ConflictSample{object_trajectory.id, ego_box, object_sample.box};
196 }
197 continue;
198 }
199
200 for (size_t sample_idx = 0; sample_idx < object_trajectory.samples.size(); ++sample_idx) {
201 const auto& object_sample = object_trajectory.samples[sample_idx];
202 const TimedBox2D* next_sample =
203 sample_idx + 1 < object_trajectory.samples.size() ? &object_trajectory.samples[sample_idx + 1] : nullptr;
204
205 TimedBox2D timed_object_sample = object_sample;
206 if (next_sample != nullptr && ego_t >= object_sample.t - object_interaction_time_window_ &&
207 ego_t <= next_sample->t + object_interaction_time_window_) {
208 if (ego_t >= object_sample.t && ego_t <= next_sample->t) {
209 timed_object_sample = interpolateTimedBox(object_sample, *next_sample, ego_t);
210 } else if (std::abs(ego_t - next_sample->t) < std::abs(ego_t - object_sample.t)) {
211 timed_object_sample = *next_sample;
212 }
213 } else if (std::abs(ego_t - object_sample.t) > object_interaction_time_window_) {
214 continue;
215 }
216
217 if (overlaps(ego_safety_box, timed_object_sample.box)) {
218 return ConflictSample{object_trajectory.id, ego_box, timed_object_sample.box};
219 }
220 }
221 }
222 }
223 }
224 return std::nullopt;
225}
226
227void SimplePlannerNode::applyObjectConstraints(const std_msgs::msg::Header& target_header,
228 const std::vector<SimplePathPoint>& base_path_points,
229 FollowRoutePlan& route_plan) {
230 if (!consider_objects_ || !object_list_init_ || route_plan.path.points.empty() || base_path_points.empty()) {
231 // Only forget the remembered speed cap if objects are disabled / never received; otherwise keep it
232 // across cycles in which the route path happens to be empty.
236 }
237 clearObjectInteractionMarkers(target_header);
238 return;
239 }
240
241 const rclcpp::Time stamp(target_header.stamp);
242 if (isMessageOutdated(object_list_.header, object_timeout_, stamp)) {
243 std::string msg =
244 "Object list is older than " + std::to_string(object_timeout_) + " seconds. Ignoring objects for this planning cycle.";
245 setHealth(diagnostic_msgs::msg::DiagnosticStatus::WARN, msg, health_.key_value_pairs);
246 RCLCPP_DEBUG(this->get_logger(), "%s", msg.c_str());
247 resetObjectState(target_header);
248 return;
249 }
250 if (object_list_.objects.empty()) {
251 resetObjectState(target_header);
252 return;
253 }
254
255 perception_msgs::msg::ObjectList tf_object_list = object_list_;
256 if (requiresTransform(object_list_.header, target_header)) {
257 try {
258 tf_object_list = tf2_buffer_->transform(object_list_, target_header.frame_id, tf2_ros::fromMsg(target_header.stamp),
259 fixed_over_time_frame_id_, tf2::durationFromSec(1.0));
260 } catch (tf2::TransformException& ex) {
261 std::string msg =
262 "Object transformation is not available: " + std::string(ex.what()) + ". Ignoring objects for this planning cycle.";
263 setHealth(diagnostic_msgs::msg::DiagnosticStatus::WARN, msg, health_.key_value_pairs);
264 RCLCPP_WARN(this->get_logger(), "%s", msg.c_str());
265 resetObjectState(target_header);
266 return;
267 }
268 }
269
270 // Ignore objects whose center is behind the ego vehicle (vehicle frame, +x ahead).
271 tf_object_list.objects.erase(
272 std::remove_if(tf_object_list.objects.begin(), tf_object_list.objects.end(),
273 [](const auto& object) { return perception_msgs::object_access::getCenterPosition(object.state).x < 0.0; }),
274 tf_object_list.objects.end());
275
276 const std::vector<ObjectTrajectory> object_trajectories = buildObjectTrajectories(tf_object_list, stamp);
277 if (object_trajectories.empty()) {
278 resetObjectState(target_header);
279 return;
280 }
281
282 double initial_speed_cap = 0.0;
283 for (const auto& point : route_plan.path.points) initial_speed_cap = std::max(initial_speed_cap, point.v);
284 if (v_ref_ >= 0.0) initial_speed_cap = std::max(initial_speed_cap, v_ref_);
285
286 // Start the search at the remembered cap and allow a single release step upwards once enough
287 // conflict-free cycles have passed (hysteresis to avoid oscillating between speeds).
288 const rclcpp::Time iteration_begin = rclcpp::Clock(RCL_SYSTEM_TIME).now();
289 const double remembered_speed_cap = std::clamp(last_object_speed_cap_.value_or(initial_speed_cap), 0.0, initial_speed_cap);
290 double search_start_speed_cap = remembered_speed_cap;
291 bool attempted_release = false;
292 if (last_object_speed_cap_.has_value() && search_start_speed_cap < initial_speed_cap &&
294 search_start_speed_cap = std::min(search_start_speed_cap + object_velocity_release_step_, initial_speed_cap);
295 attempted_release = search_start_speed_cap > remembered_speed_cap + 1e-6;
296 }
297
298 // Step the speed cap down until the resampled path is conflict-free (or reaches zero).
299 double speed_cap = search_start_speed_cap;
300 size_t iteration_count = 0;
301 std::optional<ConflictSample> last_conflict;
302 while (speed_cap > 0.0) {
303 ++iteration_count;
304 std::vector<SimplePathPoint> candidate_path =
305 resamplePath(base_path_points, route_plan.stop_at_end, route_plan.offset_to_stop_line, &speed_cap);
306 const std::optional<ConflictSample> conflict = firstConflict(candidate_path, object_trajectories);
307 if (conflict.has_value()) {
308 last_conflict = conflict;
309 speed_cap = std::max(speed_cap - object_velocity_reduction_step_, 0.0);
310 continue;
311 }
312
313 const rclcpp::Time iteration_end = rclcpp::Clock(RCL_SYSTEM_TIME).now();
315 speed_cap < initial_speed_cap ? last_conflict : std::optional<ConflictSample>{});
316 RCLCPP_DEBUG(this->get_logger(), "Object velocity iteration took %f ms (%zu iterations)",
317 (iteration_end - iteration_begin).seconds() * 1e3, iteration_count);
318
319 last_object_speed_cap_ = speed_cap < initial_speed_cap ? std::optional<double>(speed_cap) : std::nullopt;
320
321 // Hold a reduced cap for a few stable cycles before allowing the next release step upwards.
322 if (speed_cap >= initial_speed_cap - 1e-6 || speed_cap + 1e-6 < search_start_speed_cap || attempted_release) {
324 } else {
326 }
327
328 if (speed_cap <= object_standstill_speed_threshold_) {
329 health_.key_value_pairs.insert_or_assign("ObjectSpeedCap", std::to_string(speed_cap));
330 if (last_conflict.has_value()) {
331 health_.key_value_pairs.insert_or_assign("ReasonToStop", "Object conflict");
332 health_.key_value_pairs.insert_or_assign("ObjectConflictId", std::to_string(last_conflict->object_id));
333 }
334 route_plan.path.points.clear();
335 const std::string msg = "Object avoidance speed cap " + std::to_string(speed_cap) +
336 " m/s is at or below standstill threshold " + std::to_string(object_standstill_speed_threshold_) +
337 " m/s. Publishing standstill.";
338 setHealth(diagnostic_msgs::msg::DiagnosticStatus::WARN, msg, health_.key_value_pairs);
339 RCLCPP_WARN(this->get_logger(), "%s", msg.c_str());
340 return;
341 }
342
343 route_plan.path.points = candidate_path;
344 if (speed_cap < initial_speed_cap) {
345 health_.key_value_pairs.insert_or_assign("ObjectSpeedCap", std::to_string(speed_cap));
346 if (last_conflict.has_value()) {
347 health_.key_value_pairs.insert_or_assign("ObjectConflictId", std::to_string(last_conflict->object_id));
348 }
349 RCLCPP_DEBUG(this->get_logger(), "Reduced reference speed cap to %f m/s to avoid object conflict", speed_cap);
350 }
351 return;
352 }
353
354 // Speed cap reached zero while still conflicting: publish standstill.
355 route_plan.path.points = resamplePath(base_path_points, route_plan.stop_at_end, route_plan.offset_to_stop_line, &speed_cap);
356 const rclcpp::Time iteration_end = rclcpp::Clock(RCL_SYSTEM_TIME).now();
357 RCLCPP_DEBUG(this->get_logger(), "Object velocity iteration took %f ms (%zu iterations)",
358 (iteration_end - iteration_begin).seconds() * 1e3, iteration_count);
359 last_object_speed_cap_ = speed_cap;
360 const std::optional<ConflictSample> standstill_conflict = firstConflict(route_plan.path.points, object_trajectories);
361 if (standstill_conflict.has_value()) {
362 health_.key_value_pairs.insert_or_assign("ObjectSpeedCap", std::to_string(speed_cap));
363 health_.key_value_pairs.insert_or_assign("ReasonToStop", "Object conflict");
364 health_.key_value_pairs.insert_or_assign("ObjectConflictId", std::to_string(standstill_conflict->object_id));
365 last_conflict = standstill_conflict;
367 const std::string msg = "Reduced reference speed cap to 0.0 m/s; object " + std::to_string(standstill_conflict->object_id) +
368 " still conflicts at standstill. Publishing standstill.";
369 setHealth(diagnostic_msgs::msg::DiagnosticStatus::WARN, msg, health_.key_value_pairs);
370 RCLCPP_WARN(this->get_logger(), "%s", msg.c_str());
371 } else {
373 health_.key_value_pairs.insert_or_assign("ObjectSpeedCap", std::to_string(speed_cap));
374 if (last_conflict.has_value()) {
375 health_.key_value_pairs.insert_or_assign("ReasonToStop", "Object conflict");
376 health_.key_value_pairs.insert_or_assign("ObjectConflictId", std::to_string(last_conflict->object_id));
377 }
378 const std::string msg = "Reduced reference speed cap to 0.0 m/s to avoid object conflict. Publishing standstill.";
379 setHealth(diagnostic_msgs::msg::DiagnosticStatus::WARN, msg, health_.key_value_pairs);
380 RCLCPP_WARN(this->get_logger(), "%s", msg.c_str());
381 }
382 publishObjectInteractionMarkers(target_header, last_conflict);
383 route_plan.path.points.clear();
384}
385
386} // namespace simple_planner
std::vector< ObjectTrajectory > buildObjectTrajectories(const perception_msgs::msg::ObjectList &tf_object_list, const rclcpp::Time &stamp) const
Reduces the perceived object list (in vehicle frame) to timed bounding-box trajectories.
std::vector< SimplePathPoint > resamplePath(const std::vector< SimplePathPoint > &path, bool stop_at_end, double offset_to_stop_line=0.0, const double *speed_cap=nullptr)
Resamples a path into trajectory time steps and applies optional stopping behavior.
std::unique_ptr< tf2_ros::Buffer > tf2_buffer_
void applyObjectConstraints(const std_msgs::msg::Header &target_header, const std::vector< SimplePathPoint > &base_path_points, FollowRoutePlan &route_plan)
Applies trajectory-based object conflict constraints to a follow-route plan.
perception_msgs::msg::ObjectList object_list_
static constexpr double kMinObjectLength
std::optional< double > last_object_speed_cap_
void publishObjectInteractionMarkers(const std_msgs::msg::Header &target_header, const std::optional< ConflictSample > &conflict)
Publishes RViz markers for the current object interaction conflict.
rclcpp::Publisher< visualization_msgs::msg::MarkerArray >::SharedPtr object_interaction_marker_pub_
void setHealth(const unsigned char status, const std::string &msg, const std::map< std::string, std::string > &key_value_pairs={})
Sets the health information.
Definition utils.hpp:180
static constexpr double kMinObjectWidth
struct simple_planner::SimplePlannerNode::DiagnosticStatus health_
void clearObjectInteractionMarkers(const std_msgs::msg::Header &target_header)
Deletes the currently published object interaction markers.
perception_msgs::msg::EgoData ego_data_
std::optional< ConflictSample > firstConflict(const std::vector< SimplePathPoint > &ego_path, const std::vector< ObjectTrajectory > &object_trajectories) const
Returns the first conflict between the (time-sampled) ego path and any object trajectory.
static bool requiresTransform(const std_msgs::msg::Header &source_header, const std_msgs::msg::Header &target_header)
Checks whether a transform between two stamped frames is required.
Definition utils.hpp:127
static constexpr double kObjectCollisionCheckDt
void resetObjectState(const std_msgs::msg::Header &target_header)
Resets the remembered object speed cap / hysteresis state and clears interaction markers.
static bool isMessageOutdated(const std_msgs::msg::Header &header, double timeout, const rclcpp::Time &stamp)
Checks whether an input message is older than the configured timeout.
Namespace for simple_planner package.
OrientedBox2D buildOrientedBox(const Eigen::Vector2d &center, double yaw, double length, double width)
Builds an oriented 2D bounding box from center pose and dimensions.
TimedBox2D interpolateTimedBox(const TimedBox2D &lhs, const TimedBox2D &rhs, double t)
Interpolates a timed box sample at the requested relative time.
Eigen::Vector2d rotate(const Eigen::Vector2d &vec, double yaw)
Rotates a 2D vector by the given yaw angle.
double wrap_angle_rad(double angle_rad)
Wraps an angle to the range [-pi, pi].
bool overlaps(const OrientedBox2D &first_box, const OrientedBox2D &second_box)
Checks whether two oriented boxes overlap.
TimedBox2D buildObjectSample(const perception_msgs::msg::ObjectState &state, const std_msgs::msg::Header &fallback_header, const rclcpp::Time &stamp, double length, double width)
Builds a timed bounding-box sample for a single object state.
OrientedBox2D expandBoxWithSafetyMargins(const OrientedBox2D &box, double longitudinal_safety_distance, double lateral_safety_distance)
Expands a box by the given safety margins.
geometry_msgs::msg::Pose toPose(const OrientedBox2D &box)
Converts an oriented box center and heading to a ROS pose.
std::vector< TimedBox2D > samples
std::vector< SimplePathPoint > points
std::map< std::string, std::string > key_value_pairs