Skip to content

Commit 5cc5156

Browse files
committed
feat(trajectory_modifier): port V2I handling from v4.4.0.1-hotfix
Re-apply the V2I handling of #3499 on top of the trajectory_modifier imported from feat/v0.64/e2e. V2I is not on e2e, but it is enabled on v4.4.0.1 (use_v2i_remaining_time: true in the launcher), so keep it. - traffic_light_compliance_checker: add ViolationType::V2I, Parameters::v2i_handling and a use_v2i_remaining_time argument to check() (default false). The V2I logic lives in v2i_handling.cpp so that the rest of the checker stays identical to e2e. - traffic_light_stop: pass the V2I parameters to the checker and skip the check when the signals are older than signal_timeout. - Add signal_timeout and v2i_handling.* to param, struct and schema (V2I disabled by default). - Add V2I tests to the checker and a stale signal test to the plugin. Unlike #3499, allow_if_cannot_stop uses the distance from the ego front, as the e2e red and amber checks do (#3380).
1 parent 2367b82 commit 5cc5156

11 files changed

Lines changed: 522 additions & 9 deletions

File tree

‎common/autoware_traffic_light_compliance_checker/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ament_auto_add_library(${PROJECT_NAME} SHARED
88
src/traffic_light_compliance_checker.cpp
99
src/traffic_light_status_tracker.cpp
1010
src/utils.cpp
11+
src/v2i_handling.cpp
1112
)
1213

1314
if(BUILD_TESTING)

‎common/autoware_traffic_light_compliance_checker/include/autoware/traffic_light_compliance_checker/structs.hpp‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ struct StopLineInfo
5050
};
5151

5252
/// @brief type of traffic light violation
53-
enum class ViolationType { RED_LIGHT, AMBER_LIGHT };
53+
enum class ViolationType { RED_LIGHT, AMBER_LIGHT, V2I };
5454

5555
/// @brief violation detail
5656
struct Violation
@@ -125,6 +125,13 @@ struct Parameters
125125
double hysteresis_duration{0.0};
126126
bool reject_if_stop_detected{false};
127127
} amber_rejection;
128+
struct V2IHandling
129+
{
130+
bool get_last_time_allowed_to_pass_from_map{false};
131+
double last_time_allowed_to_pass{4.0};
132+
double velocity_threshold{0.5};
133+
double required_time_to_departure{5.0};
134+
} v2i_handling;
128135
};
129136

130137
} // namespace autoware::traffic_light_compliance_checker

‎common/autoware_traffic_light_compliance_checker/include/autoware/traffic_light_compliance_checker/traffic_light_compliance_checker.hpp‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ class TrafficLightComplianceChecker
5555
/**
5656
* @brief check if the trajectory complies with traffic lights
5757
* @param input input data for compliance check (raw signals are filtered internally)
58+
* @param use_v2i_remaining_time also reject crossings of non-stop lights that V2I predicts to
59+
* turn red before ego can pass
5860
* @return result of compliance check, or error message if check fails
5961
*/
6062
[[nodiscard]] tl::expected<ComplianceResult, std::string> check(
61-
const Inputs & input, const bool check_red_lights = true, const bool check_amber_lights = true);
63+
const Inputs & input, const bool check_red_lights = true, const bool check_amber_lights = true,
64+
const bool use_v2i_remaining_time = false);
6265

6366
/**
6467
* @brief update parameters
@@ -110,6 +113,13 @@ class TrafficLightComplianceChecker
110113

111114
bool is_allow_if_cannot_stop(const double distance_from_ego_front) const;
112115

116+
/// @brief return violations for the stop lines of non-stop lights that will turn red before ego
117+
/// can pass them, based on the V2I signal predictions. Defined in v2i_handling.cpp.
118+
[[nodiscard]] Violations get_v2i_violations(
119+
const Inputs & input,
120+
const autoware_perception_msgs::msg::TrafficLightGroupArray & filtered_signals,
121+
const bool is_ego_stopped) const;
122+
113123
void cleanup_violation_distance_history(
114124
const std::vector<StopLineInfo> & red_stop_lines,
115125
const std::vector<StopLineInfo> & amber_stop_lines, const bool check_red_lights,

‎common/autoware_traffic_light_compliance_checker/src/traffic_light_compliance_checker.cpp‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ void TrafficLightComplianceChecker::update_parameters(const Parameters & paramet
125125
}
126126

127127
tl::expected<ComplianceResult, std::string> TrafficLightComplianceChecker::check(
128-
const Inputs & input, const bool check_red_lights, const bool check_amber_lights)
128+
const Inputs & input, const bool check_red_lights, const bool check_amber_lights,
129+
const bool use_v2i_remaining_time)
129130
{
130131
if (input.map == nullptr) {
131132
return tl::make_unexpected("Lanelet map is not set");
@@ -148,6 +149,11 @@ tl::expected<ComplianceResult, std::string> TrafficLightComplianceChecker::check
148149
input, filtered_signals, force_reject_amber_ids, check_red_lights, check_amber_lights,
149150
is_ego_stopped);
150151

152+
if (use_v2i_remaining_time) {
153+
const auto v2i_violations = get_v2i_violations(input, filtered_signals, is_ego_stopped);
154+
result.violations.insert(result.violations.end(), v2i_violations.begin(), v2i_violations.end());
155+
}
156+
151157
cleanup_amber_rejection_history(input.current_time);
152158

153159
return result;
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)