|
| 1 | +// Copyright 2026 TIER IV, Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +// V2I handling of the traffic light compliance checker, ported from v4.4.0.1-hotfix (#3499). |
| 16 | +// It is kept in its own file so that the rest of the checker stays identical to feat/v0.64/e2e. |
| 17 | + |
| 18 | +#include "autoware/traffic_light_compliance_checker/traffic_light_compliance_checker.hpp" |
| 19 | + |
| 20 | +#include <autoware/interpolation/linear_interpolation.hpp> |
| 21 | +#include <autoware/traffic_light_utils/traffic_light_utils.hpp> |
| 22 | +#include <autoware_utils_geometry/geometry.hpp> |
| 23 | +#include <rclcpp/duration.hpp> |
| 24 | + |
| 25 | +#include <boost/geometry.hpp> |
| 26 | + |
| 27 | +#include <lanelet2_core/geometry/Point.h> |
| 28 | +#include <lanelet2_core/primitives/BasicRegulatoryElements.h> |
| 29 | + |
| 30 | +#include <algorithm> |
| 31 | +#include <optional> |
| 32 | +#include <unordered_map> |
| 33 | +#include <vector> |
| 34 | + |
| 35 | +namespace |
| 36 | +{ |
| 37 | +using autoware::traffic_light_compliance_checker::StopLineInfo; |
| 38 | +using autoware_perception_msgs::msg::PredictedTrafficLightState; |
| 39 | +using autoware_perception_msgs::msg::TrafficLightGroup; |
| 40 | +using autoware_planning_msgs::msg::TrajectoryPoint; |
| 41 | + |
| 42 | +/// @brief stop line of a light that does not require a stop now, with its route lanelet and signal |
| 43 | +struct V2IStopLine |
| 44 | +{ |
| 45 | + StopLineInfo info; |
| 46 | + lanelet::ConstLanelet lanelet; |
| 47 | + TrafficLightGroup signal; |
| 48 | +}; |
| 49 | + |
| 50 | +/// @brief part of the trajectory ahead of ego, up to the first stop point |
| 51 | +struct V2ITrajectory |
| 52 | +{ |
| 53 | + std::vector<TrajectoryPoint> points; |
| 54 | + lanelet::BasicLineString2d linestring; |
| 55 | + std::optional<lanelet::BasicPoint2d> stop_point; |
| 56 | + double backward_length{0.0}; |
| 57 | +}; |
| 58 | + |
| 59 | +/// @brief crossing of a stop line by the trajectory |
| 60 | +struct StopLineCrossing |
| 61 | +{ |
| 62 | + double distance; |
| 63 | + double time; |
| 64 | + lanelet::BasicPoint2d point; |
| 65 | +}; |
| 66 | + |
| 67 | +/// @brief get the stop lines on the route whose light does not require a stop now |
| 68 | +std::vector<V2IStopLine> collect_v2i_stop_lines( |
| 69 | + const lanelet::LaneletMap & lanelet_map, const autoware_planning_msgs::msg::LaneletRoute & route, |
| 70 | + const std::vector<TrafficLightGroup> & traffic_light_groups) |
| 71 | +{ |
| 72 | + std::unordered_map<lanelet::Id, lanelet::Id> route_lanelet_id_per_traffic_light_id; |
| 73 | + for (const auto & segment : route.segments) { |
| 74 | + for (const auto & tl : lanelet_map.laneletLayer.get(segment.preferred_primitive.id) |
| 75 | + .regulatoryElementsAs<lanelet::TrafficLight>()) { |
| 76 | + route_lanelet_id_per_traffic_light_id.emplace(tl->id(), segment.preferred_primitive.id); |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + std::vector<V2IStopLine> stop_lines; |
| 81 | + for (const auto & signal : traffic_light_groups) { |
| 82 | + const auto hit = route_lanelet_id_per_traffic_light_id.find(signal.traffic_light_group_id); |
| 83 | + if (hit == route_lanelet_id_per_traffic_light_id.end()) { |
| 84 | + continue; |
| 85 | + } |
| 86 | + const auto traffic_light_it = |
| 87 | + lanelet_map.regulatoryElementLayer.find(signal.traffic_light_group_id); |
| 88 | + if (traffic_light_it == lanelet_map.regulatoryElementLayer.end()) { |
| 89 | + continue; |
| 90 | + } |
| 91 | + const lanelet::ConstLanelet lanelet = lanelet_map.laneletLayer.get(hit->second); |
| 92 | + if (autoware::traffic_light_utils::isTrafficSignalStop(lanelet, signal)) { |
| 93 | + continue; // already handled by the red and amber checks |
| 94 | + } |
| 95 | + const auto traffic_light = |
| 96 | + std::dynamic_pointer_cast<const lanelet::TrafficLight>(*traffic_light_it); |
| 97 | + if (!traffic_light || !traffic_light->stopLine().has_value()) { |
| 98 | + continue; |
| 99 | + } |
| 100 | + stop_lines.push_back( |
| 101 | + {StopLineInfo{ |
| 102 | + lanelet::utils::to2D(traffic_light->stopLine()->basicLineString()), |
| 103 | + signal.traffic_light_group_id}, |
| 104 | + lanelet, signal}); |
| 105 | + } |
| 106 | + return stop_lines; |
| 107 | +} |
| 108 | + |
| 109 | +/// @brief prepare the full trajectory ahead of ego, as a V2I prediction can concern a stop line far |
| 110 | +/// ahead. Return nullopt if it has less than two points, as it cannot cross a stop line. |
| 111 | +std::optional<V2ITrajectory> prepare_v2i_trajectory( |
| 112 | + const std::vector<TrajectoryPoint> & input_trajectory, const double stopped_velocity_threshold, |
| 113 | + const double max_longitudinal_offset, const bool is_ego_stopped) |
| 114 | +{ |
| 115 | + if (input_trajectory.empty()) { |
| 116 | + return std::nullopt; |
| 117 | + } |
| 118 | + |
| 119 | + // same forward offset as the red and amber checks, to allow more gap when stopped |
| 120 | + const auto forward_stop_point_offset = is_ego_stopped ? 0.25 : 0.0; |
| 121 | + |
| 122 | + V2ITrajectory trajectory; |
| 123 | + auto length = 0.0; |
| 124 | + auto last_p = input_trajectory.front(); |
| 125 | + for (const auto & p : input_trajectory) { |
| 126 | + // skip points behind ego |
| 127 | + if (rclcpp::Duration(p.time_from_start).seconds() < 0.0) { |
| 128 | + trajectory.backward_length += autoware_utils_geometry::calc_distance2d(last_p.pose, p.pose); |
| 129 | + last_p = p; |
| 130 | + continue; |
| 131 | + } |
| 132 | + const lanelet::BasicPoint2d lanelet_p(p.pose.position.x, p.pose.position.y); |
| 133 | + if (!trajectory.linestring.empty()) |
| 134 | + length += lanelet::geometry::distance2d(trajectory.linestring.back(), lanelet_p); |
| 135 | + trajectory.points.push_back(p); |
| 136 | + trajectory.linestring.emplace_back(lanelet_p); |
| 137 | + |
| 138 | + if ( |
| 139 | + length > forward_stop_point_offset && |
| 140 | + p.longitudinal_velocity_mps <= stopped_velocity_threshold) { |
| 141 | + trajectory.stop_point = trajectory.linestring.back(); |
| 142 | + break; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + if (trajectory.linestring.size() < 2) { |
| 147 | + return std::nullopt; |
| 148 | + } |
| 149 | + |
| 150 | + if (max_longitudinal_offset > 0.0) { |
| 151 | + // extend the trajectory linestring by the vehicle's longitudinal offset |
| 152 | + const auto offset_pose = autoware_utils_geometry::calc_offset_pose( |
| 153 | + trajectory.points.back().pose, max_longitudinal_offset, 0.0, 0.0); |
| 154 | + const lanelet::BasicPoint2d offset_point(offset_pose.position.x, offset_pose.position.y); |
| 155 | + trajectory.linestring.emplace_back(offset_point); |
| 156 | + if (trajectory.stop_point.has_value()) trajectory.stop_point.value() = offset_point; |
| 157 | + } |
| 158 | + return trajectory; |
| 159 | +} |
| 160 | + |
| 161 | +/// @brief return where and when the trajectory crosses the stop line, or nullopt if it does not |
| 162 | +std::optional<StopLineCrossing> find_stop_line_crossing( |
| 163 | + const V2ITrajectory & trajectory, const lanelet::BasicLineString2d & stop_line) |
| 164 | +{ |
| 165 | + const auto & points = trajectory.points; |
| 166 | + const auto & ls = trajectory.linestring; |
| 167 | + auto distance = 0.0; |
| 168 | + for (size_t i = 0; i + 1 < ls.size(); ++i) { |
| 169 | + lanelet::BasicPoints2d intersection_points; |
| 170 | + const lanelet::BasicLineString2d segment{ls[i], ls[i + 1]}; |
| 171 | + const auto segment_length = static_cast<double>(boost::geometry::length(segment)); |
| 172 | + boost::geometry::intersection(segment, stop_line, intersection_points); |
| 173 | + if (intersection_points.empty()) { |
| 174 | + distance += segment_length; |
| 175 | + continue; |
| 176 | + } |
| 177 | + const auto distance_to_intersection = |
| 178 | + boost::geometry::distance(segment.front(), intersection_points.front()); |
| 179 | + const auto ratio = distance_to_intersection / segment_length; |
| 180 | + // the last segment models the vehicle body at the last trajectory point. it has no time of its |
| 181 | + // own, so use the time of that point. |
| 182 | + const auto next_index = std::min(i + 1, points.size() - 1); |
| 183 | + const auto time = autoware::interpolation::lerp( |
| 184 | + rclcpp::Duration(points[i].time_from_start).seconds(), |
| 185 | + rclcpp::Duration(points[next_index].time_from_start).seconds(), ratio); |
| 186 | + return StopLineCrossing{distance + distance_to_intersection, time, intersection_points.front()}; |
| 187 | + } |
| 188 | + return std::nullopt; |
| 189 | +} |
| 190 | + |
| 191 | +/// @brief return true if a prediction turns the signal to stop less than time_limit seconds from |
| 192 | +/// now. Predictions in the past are ignored, because they come from a stale feed or an unset stamp. |
| 193 | +bool turns_red_within( |
| 194 | + const std::vector<PredictedTrafficLightState> & predictions, |
| 195 | + const lanelet::ConstLanelet & lanelet, const rclcpp::Time & current_time, const double time_limit) |
| 196 | +{ |
| 197 | + // tolerance for small clock differences between the V2I source and ego [s] |
| 198 | + constexpr double past_prediction_tolerance = 0.5; |
| 199 | + return std::any_of(predictions.begin(), predictions.end(), [&](const auto & prediction) { |
| 200 | + if (!autoware::traffic_light_utils::isTrafficSignalStop( |
| 201 | + lanelet, prediction.simultaneous_elements)) { |
| 202 | + return false; |
| 203 | + } |
| 204 | + const auto time_to_red = (rclcpp::Time(prediction.predicted_stamp) - current_time).seconds(); |
| 205 | + if (time_to_red < -past_prediction_tolerance) { |
| 206 | + return false; |
| 207 | + } |
| 208 | + return time_to_red < time_limit; |
| 209 | + }); |
| 210 | +} |
| 211 | +} // namespace |
| 212 | + |
| 213 | +namespace autoware::traffic_light_compliance_checker |
| 214 | +{ |
| 215 | + |
| 216 | +Violations TrafficLightComplianceChecker::get_v2i_violations( |
| 217 | + const Inputs & input, |
| 218 | + const autoware_perception_msgs::msg::TrafficLightGroupArray & filtered_signals, |
| 219 | + const bool is_ego_stopped) const |
| 220 | +{ |
| 221 | + const auto trajectory = prepare_v2i_trajectory( |
| 222 | + input.trajectory, params_.ego_stopped_velocity_threshold, |
| 223 | + vehicle_info_.max_longitudinal_offset_m, is_ego_stopped); |
| 224 | + if (!trajectory) { |
| 225 | + return {}; |
| 226 | + } |
| 227 | + |
| 228 | + const auto & v2i_params = params_.v2i_handling; |
| 229 | + Violations violations; |
| 230 | + for (const auto & stop_line : |
| 231 | + collect_v2i_stop_lines(*input.map, input.route, filtered_signals.traffic_light_groups)) { |
| 232 | + const auto crossing = find_stop_line_crossing(*trajectory, stop_line.info.line); |
| 233 | + const auto ego_front_to_stop_line = |
| 234 | + crossing ? crossing->distance - vehicle_info_.max_longitudinal_offset_m : 0.0; |
| 235 | + if ( |
| 236 | + !crossing || |
| 237 | + is_stop_point_within_margin_from_stop_line(trajectory->stop_point, stop_line.info.line) || |
| 238 | + is_allow_if_cannot_stop(ego_front_to_stop_line)) { |
| 239 | + continue; |
| 240 | + } |
| 241 | + |
| 242 | + // a slow ego needs a fixed departure time instead of the trajectory crossing time |
| 243 | + const double time_to_cross_stop_line = input.current_velocity > v2i_params.velocity_threshold |
| 244 | + ? crossing->time |
| 245 | + : v2i_params.required_time_to_departure; |
| 246 | + const double last_time_allowed_to_pass = |
| 247 | + v2i_params.get_last_time_allowed_to_pass_from_map |
| 248 | + ? stop_line.lanelet.attributeOr( |
| 249 | + "v2i_last_time_allowed_to_pass", v2i_params.last_time_allowed_to_pass) |
| 250 | + : v2i_params.last_time_allowed_to_pass; |
| 251 | + if (!turns_red_within( |
| 252 | + stop_line.signal.predictions, stop_line.lanelet, input.current_time, |
| 253 | + time_to_cross_stop_line + last_time_allowed_to_pass)) { |
| 254 | + continue; |
| 255 | + } |
| 256 | + |
| 257 | + violations.emplace_back( |
| 258 | + ViolationType::V2I, stop_line.info.line, stop_line.info.traffic_light_id, crossing->point, |
| 259 | + crossing->distance + trajectory->backward_length); |
| 260 | + } |
| 261 | + return violations; |
| 262 | +} |
| 263 | + |
| 264 | +} // namespace autoware::traffic_light_compliance_checker |
0 commit comments