Skip to content

feat(autoware_carla_interface): publish CARLA traffic-light states, matched to the map by position - #13327

Open
hakuturu583 wants to merge 7 commits into
autowarefoundation:mainfrom
hakuturu583:feat/carla-traffic-light-position-match
Open

feat(autoware_carla_interface): publish CARLA traffic-light states, matched to the map by position#13327
hakuturu583 wants to merge 7 commits into
autowarefoundation:mainfrom
hakuturu583:feat/carla-traffic-light-position-match

Conversation

@hakuturu583

Copy link
Copy Markdown
Contributor

Description

Bridges the CARLA server's traffic-light states into Autoware's perception output, so a CARLA
closed loop can run without camera-based traffic-light recognition.

This reworks the approach explored in #13309. The core problem is how a CARLA traffic light is
associated with an Autoware traffic_light_group_id
(the id of a traffic_light regulatory
element in the lanelet2 map). Rather than assume the CARLA OpenDRIVE signal id equals the
regulatory-element id — which only holds for maps auto-generated straight from the same OpenDRIVE —
or require a hand-written id table, the bridge discovers the mapping geometrically: each CARLA
light head is matched to the nearest lanelet2 light head and its state is published under every
regulatory element that references that head. This makes the feature work for hand-authored / TIER
IV Vector Map Builder maps, whose regulatory-element ids do not correspond to the OpenDRIVE signal
ids.

What's added

Launch arguments (all off / empty by default, so existing behavior is unchanged):

launch arg node param effect
publish_traffic_lights traffic_light.publish Publish autoware_perception_msgs/TrafficLightGroupArray on /perception/traffic_light_recognition/traffic_signals every tick.
force_green_traffic_lights traffic_light.force_green At startup, set every CARLA traffic light to green and freeze it (camera-less closed loops that would otherwise hold at every stop line).
traffic_light_map_path traffic_light.map_path lanelet2 .osm used for position matching.
traffic_light_match_distance traffic_light.match_distance Max head-to-head match distance (default 5.0 m).
traffic_light_match_ratio traffic_light.match_ratio Ambiguity threshold (default 0.6).
traffic_light_id_map traffic_light.id_map Optional opendrive_id:group_id,... override that pins a light, taking precedence over matching.

The node parameters are grouped under a traffic_light. namespace so they stay together in
ros2 param list.

How the matching works

modules/traffic_light_matcher.py (ROS-free, unit-tested):

  • Parses the .osm directly as XML, reading each node's local_x/local_y tags — the
    Autoware map-frame coordinates (for an MGRS map, exactly the easting/northing the map loader
    produces). This avoids depending on the lanelet2 C++ regulatory-element registration and
    sidesteps any projector mismatch between this process and the map loader.
  • A physical head is a refers way, commonly shared by several regulatory elements (one per
    approaching lane / stop line), so each head carries the set of group ids that reference it; a
    matched CARLA light publishes its state under all of them.
  • Each CARLA head is expressed in the map frame from the mean of its get_light_boxes() centres
    via the existing carla_location_to_ros_point(map_origin_x, map_origin_y) transform.
  • Conservative binding. A light is bound only when a single head is clearly closest. If a head
    resolving to a different signal is nearly as close (nearest > ratio * second, the "light
    across the intersection" case) or nothing is within match_distance, the light is left
    unpublished and logged as ambiguous / too-far — never guessed. The startup log prints a match
    report (N matched, M ambiguous, K too far); reported lights can be pinned via
    traffic_light_id_map.

Resolution precedence per light: traffic_light.id_map override → position match (map_path) →
OpenDRIVE-id-as-group-id fallback (no map path).

How was this PR tested?

  • test/test_traffic_light_matcher.py — unit tests covering shared-head group expansion, isolated
    matches, the ambiguity guard (both the disjoint-signal midpoint that must be dropped and the
    close same-group / clear-winner neighbours that must not), too-far, and .osm parsing.
  • Validated the parser and matcher against the Odaiba lanelet2 map (946 heads / 906 groups): with
    realistic alignment (~0.3 m) ~97 % of heads match, and genuinely-ambiguous sub-metre distinct
    signals are reported rather than mis-assigned.
  • black (line-length 100) clean; dotted parameter names verified to declare / resolve under
    rclpy.
  • No behavioral change unless publish_traffic_lights (and, for matching, traffic_light_map_path)
    is set.

@github-actions github-actions Bot added type:documentation Creating or refining documentation. (auto-assigned) component:simulation Virtual environment setups and simulations. (auto-assigned) labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thank you for contributing to the Autoware project!

🚧 If your pull request is in progress, switch it to draft mode.

Please ensure:

@hakuturu583
hakuturu583 marked this pull request as draft September 4, 2026 07:46
@hakuturu583
hakuturu583 force-pushed the feat/carla-traffic-light-position-match branch from 9d538e5 to 706b02c Compare September 4, 2026 07:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d538e50b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread simulator/autoware_carla_interface/src/autoware_carla_interface/carla_ros.py Outdated
@hakuturu583 hakuturu583 added the run:build-and-test-differential Mark to enable build-and-test-differential workflow. (used-by-ci) label Sep 4, 2026
Masaya Kataoka and others added 2 commits September 4, 2026 17:50
…atched to the map by position

Bridge the CARLA server's traffic-light states into Autoware's perception
output so a CARLA closed loop can run without camera-based recognition.

The key problem is associating a CARLA traffic light with an Autoware
`traffic_light_group_id` (a `traffic_light` regulatory-element id in the
lanelet2 map). Instead of assuming the CARLA OpenDRIVE signal id equals the
regulatory-element id (true only for maps auto-generated from the same
OpenDRIVE) or hand-writing an id table, the bridge discovers the mapping
geometrically: each CARLA light head is matched to the nearest lanelet2 light
head and its state is published under every regulatory element that references
that head. This works for hand-authored / Vector Map Builder maps too.

- New `modules/traffic_light_matcher.py`: parses the lanelet2 `.osm` directly
  (reads `local_x`/`local_y`, i.e. the map frame; no lanelet2/projector
  dependency), keys physical heads by their `refers` way, and matches CARLA
  heads conservatively (distance threshold + a disjoint-group ambiguity ratio),
  dropping and logging ambiguous / too-far lights rather than guessing.
- `carla_ros.py`: publishes `TrafficLightGroupArray` on
  /perception/traffic_light_recognition/traffic_signals, aggregated per group.
- `carla_autoware.py`: `force_green` freezes all lights green for camera-less
  runs.
- Parameters grouped under the `traffic_light.` namespace; resolution order is
  id-map override -> position match -> OpenDRIVE-id fallback.
- Unit tests for the matcher; README documents the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
…matching

- Use the resolved map origin (`_current_map_origin()`) instead of the raw
  `map_origin_x/y` parameters when placing CARLA light heads in the map frame,
  so georeferenced maps (origin derived from the OpenDRIVE geoReference in
  `on_world_ready`, parameters left at zero) match correctly instead of falling
  outside the distance threshold and publishing nothing. (P1)
- Treat a candidate head as a genuine alternative for the ambiguity test unless
  its group set is exactly equal to the winner's, replacing the `isdisjoint`
  check. Overlapping-but-unequal sets (e.g. {500, 501} vs {501}) would otherwise
  be accepted by arbitrary ranking and publish a missing or spurious group. Adds
  a regression test. (P2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
@hakuturu583
hakuturu583 force-pushed the feat/carla-traffic-light-position-match branch from 46c8693 to da0463e Compare September 4, 2026 08:51
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 20.49%. Comparing base (f540393) to head (3500c33).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #13327   +/-   ##
=======================================
  Coverage   20.49%   20.49%           
=======================================
  Files        1948     1948           
  Lines      137202   137170   -32     
  Branches    48933    48932    -1     
=======================================
- Hits        28113    28109    -4     
+ Misses      86314    86283   -31     
- Partials    22775    22778    +3     
Flag Coverage Δ *Carryforward flag
daily ?
full-suite 20.49% <ø> (+<0.01%) ⬆️ Carriedforward from caea266

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hakuturu583
hakuturu583 marked this pull request as ready for review September 4, 2026 11:42
@hakuturu583
hakuturu583 enabled auto-merge (squash) September 4, 2026 11:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da0463e0ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread simulator/autoware_carla_interface/src/autoware_carla_interface/carla_ros.py Outdated
@Max-Bin

Max-Bin commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@hakuturu583 Hi, thanks for the PR. Could please help solve the failed CodeScene Check?

…cene

Address the second review round:

- traffic_light.id_map now accepts several group ids per OpenDRIVE signal id,
  formatted `opendrive_id:group_id[|group_id...],...` (repeated keys merge). A
  physical head shared by multiple regulatory elements can therefore be pinned
  to all of them via one override, matching the position matcher's shared-head
  behaviour, instead of only ever recovering a single group. (Codex P2)
- Reduce complexity flagged by the CodeScene gate by extracting helpers so the
  hot methods stay flat and short:
  - traffic_light_matcher: split `load_map_traffic_lights` into node/way/relation
    parsers (Complex Method / Bumpy Road) and pull the per-head decision out of
    `match_traffic_lights` into `_classify_head` (Large Method).
  - carla_ros: split `_resolve_traffic_light_groups` into `_apply_id_map_override`
    / `_match_actors_to_map` / `_fallback_opendrive_groups`; the override parser
    is now the pure, unit-tested `parse_id_map_override`.
  - carla_autoware: fold the enable check into `_force_green_traffic_lights` so
    `load_world` keeps a single unconditional call and gains no branch.
- Extend unit tests for the multi-id override parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
@hakuturu583

Copy link
Copy Markdown
Contributor Author

@Max-Bin thanks for the review! I've addressed the failing CodeScene gate in c52ece1 by refactoring the methods it flagged (rather than suppressing them):

  • traffic_light_matcher.pyload_map_traffic_lights (Complex Method / Bumpy Road) is split into small _parse_local_nodes / _parse_ways / _is_traffic_light_relation / _refers_way_ids / _way_centroid helpers, and the per-head decision is pulled out of match_traffic_lights (Large Method) into _classify_head.
  • carla_ros.py_resolve_traffic_light_groups (Complex Method / Bumpy Road) is split into _apply_id_map_override / _match_actors_to_map / _fallback_opendrive_groups.
  • carla_autoware.py — the force_green enable check is folded into _force_green_traffic_lights so load_world keeps a single unconditional call and gains no extra branch.

The same commit also addresses the Codex P2 comment (id_map now supports multiple group ids per signal). Tests still pass and black is clean; CI should re-run the CodeScene check on the new commit.

Masaya Kataoka and others added 4 commits September 7, 2026 10:12
Follow-up to the previous refactor, targeting the two rules still flagged on the
traffic-light matcher:

- Extract `_add_relation_heads` so `load_map_traffic_lights` is a single flat loop
  with no nested conditional block (clears Bumpy Road Ahead).
- Pass the per-run matching state as one `_MatchContext` namedtuple instead of a
  seven-parameter list, so `_classify_head(head, ctx)` takes two arguments (clears
  Excess Number of Function Arguments).

No behavioural change; tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
…own table)

On top of the bot autofix, finish the parts it could not:

- Capitalize the first word of the parser-helper docstrings (flake8 / pydocstyle D403).
- Order the traffic_light_matcher imports as isort (profile=black) expects.
- Remove the literal `|` from the traffic_light.id_map row of the README parameter
  table (it split the cell and broke the column count; the bot's --fix left it mangled).
  The `|`-separated multi-id syntax stays documented in prose. markdownlint and prettier
  now pass on the table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
item = item.strip()
if not item:
continue
opendrive_str, groups_str = item.split(":")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we validate malformed traffic_light.id_map entries instead of letting split()/int() raise? A typo like 12 terminatesn the bridge on the first simulation tick, while 12: silently overrides the actor with an empty group list. A clear warning for the offending entry would make this safer to configure.

element = TrafficLightElement()
element.color = color
element.shape = TrafficLightElement.CIRCLE
element.status = TrafficLightElement.SOLID_ON

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the CARLA state here? TrafficLightState.Off is converted to UNKNOWN, but the message is still published as SOLID_ON. That produces an “unknown-color, on” signal for a known-off lamp. Please map Off to SOLID_OFF and truly unknown states to UNKNOWN.

None,
)
entry["second"] = second_dist
if second_dist is not None and nearest_dist > ctx.ambiguity_ratio * second_dist:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if two different group sets have exactly the same centroid, both distances are 0, so 0 > ratio * 0 is false and the result depends on XML order. Could we treat equal-distance candidates, especially second_dist == 0, as ambiguous?

<arg name="sensor_mapping_file" default="$(var sensor_mapping_file_default)" description="Path to sensor mapping YAML"/>
<arg name="flatten_steering_curve" default="false" description="Replace the ego vehicle's speed-based steering curve with an identity curve (CARLA 0.10 corrupt-curve workaround)"/>
<arg name="wake_sleeping_physics" default="false" description="Nudge the ego physics body awake when launching from standstill (CARLA 0.10 sleeping-body workaround; not needed on 0.9.15)"/>
<arg name="traffic_light.publish" default="false" description="Publish CARLA traffic-light states on /perception/traffic_light_recognition/traffic_signals"/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The launch argument names in the PR description (publish_traffic_lights, traffic_light_map_path, etc.) do not match the arguments actually declared here (traffic_light.publish, traffic_light.map_path, etc.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:simulation Virtual environment setups and simulations. (auto-assigned) run:build-and-test-differential Mark to enable build-and-test-differential workflow. (used-by-ci) tag:require-cuda-build-and-test type:documentation Creating or refining documentation. (auto-assigned)

Projects

Status: To Triage

Development

Successfully merging this pull request may close these issues.

2 participants