Skip to content

radard: fuse vision's lead speed into the matched radar lead - #39021

Closed
ds-sebastian wants to merge 1 commit into
commaai:masterfrom
ds-sebastian:radard-vision-fusion
Closed

ds-sebastian wants to merge 1 commit into
commaai:masterfrom
ds-sebastian:radard-vision-fusion

Conversation

@ds-sebastian

Copy link
Copy Markdown

Purpose

Some radars report short, smooth excursions in a lead's relative speed (1–10 s) that the lead's own range does not follow. radard's per-track filter only sees vLead, so the excursion goes straight into vLeadK / aLeadK and the planner: unnecessary slowdowns and a rougher ride. This PR lets the vision-matched lead track take vision's lead speed as a second, lower-weight measurement, so excursions that vision does not see are damped instead of passed on.

Change (one file, +59 −6)

  • Track's fixed-gain KF1D [vLead, aLead] is rewritten as the same filter in covariance form: a one-step predictor with R = 1 (m/s)² and Q = diag(0.2, 2.0)·dt, whose steady-state predictor gain equals the existing KalmanParams table exactly. Radar-only behaviour is unchanged: it matches KF1D to 3e-8 on a synthetic sequence, and replay output is identical with fusion switched off. The update is plain floats and costs 0.42 µs per update vs 0.60 µs for KF1D. (Side note: the comment's Q = diag(10, 100), R = 1e3 does not reproduce the table; Q = diag(0.2, 2.0)·dt, R = 1 does.)
  • Track.fuse_vision(): for the track matched to leadOne only (vision prob > 0.5, matching unchanged), fuse v_ego + lead.v[0] − model_v_ego with σ = VISION_V_STD_SCALE · max(vStd, 0.1), where VISION_V_STD_SCALE = 2. Where radar and vision agree, nothing changes.

Verification

  • Setup: open-loop process_replay of card → radard → plannerd on 0.11.2 (radard.py identical to current master) with a Toyota RAV4 2023, which has a Continental ARS510 radar (decoder: ds-sebastian/ars510-radar).
  • Selection and decision: variants were chosen on 1.1 h of development drives. The decision rule was pre-registered before the held-out run: 20 held-out drive chains (4.6 h), scored against the human driver's own longitudinal behaviour. Further closed-loop drives were kept aside as a final check.
driver-agreement error Δ [95% CI] reaction to driver braking radar-only brake requests (driver on gas) lead switches
this PR, held-out −0.0043 m/s² [−0.0068, −0.0018], Holm p < 0.001 +0.085 s [+0.004, +0.177] 0.88/h (unchanged) unchanged
this PR, further drives −0.0005 [−0.0009, +0.0004] (not worse) +0.004 s 0 (unchanged) unchanged

Radar still reacts earlier than vision-only to real slowdowns.

On the development drives, two other radard ideas did not help:

  • adding dRel to the track filter: no gain, because range is too noisy on this radar;
  • down-weighting aLeadK on radar/vision disagreement: it cost anticipation.

Limits and open questions

  • Scope: tested on one radar (ARS510) and one recorded vision model, in open-loop replay, with one driver as the yardstick. It changes radarState for every radar car, so process-replay refs will change and it needs checking on other radar platforms.
  • The vision weight is the key knob. Refereed by the radar's own 4 s range slope, vision speed is roughly 2× noisier than this radar's (median 1.1–2.0 vs 0.6–1.2 m/s at 20–120 m). Vision's vStd tracks that only loosely. In the sustained false-closing episodes, though, range sided with vision in 22 of 33 cases. A larger VISION_V_STD_SCALE (3–4) may be the better trade for radars with stronger velocity.
  • It costs about 0.09 s of reaction on this car.

I understand this may be out of scope for stock openpilot (a behaviour change validated on one platform). It is a small, self-contained change that forks using radar leads may find useful, so I'm opening it as a draft for visibility and feedback.

Rewrite Track's fixed-gain KF1D as the same [vLead, aLead] filter in
covariance form (one-step predictor, R = 1, Q = diag(0.2, 2.0) * dt, which
reproduces the KalmanParams gain table exactly), so radar-only tracks are
unchanged. For the track matched to leadOne, fuse vision's lead speed as a
second measurement with sigma = 2 * vStd, so radar speed excursions that
vision does not see are damped instead of passed to the planner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ds-sebastian

Copy link
Copy Markdown
Author

Opened by mistake, sorry for the noise. Closing.

@ds-sebastian
ds-sebastian deleted the radard-vision-fusion branch September 27, 2026 04:14
@github-actions

Copy link
Copy Markdown
Contributor

Process replay diff report

Replays driving segments through this PR and compares the behavior to master.
Please review any changes carefully to ensure they are expected.

⚠️ 1 changed, 65 passed, 0 errors

Show changes

TOYOTA - regen218A4DCFAA1|2025-04-08--22-57-51--0 [radard]

  radarState.leadOne.aLeadK (963 diffs)
    frame 17: 0.03307333588600159 -> 0.018868228420615196
    frame 18: 0.04250093176960945 -> 0.03212708234786987
    frame 19: 0.04532938078045845 -> 0.03255578503012657
    frame 20: 0.04757510870695114 -> 0.05466168373823166
    frame 21: 0.044773854315280914 -> 0.06348730623722076
    frame 22: 0.04618648812174797 -> 0.06955883651971817
    frame 23: 0.04937124252319336 -> 0.05970412865281105
    frame 24: 0.060453206300735474 -> 0.09425348043441772
    frame 25: 0.07742370665073395 -> 0.11312757432460785
    frame 26: 0.08860228210687637 -> 0.1405457705259323
    (... 953 more)

  radarState.leadOne.aLeadTau (169 diffs)
    frame 107: 1.350000023841858 -> 1.5
    frame 249: 1.350000023841858 -> 1.5
    frame 261: 1.350000023841858 -> 1.5
    frame 262: 1.215000033378601 -> 1.350000023841858
    frame 263: 1.093500018119812 -> 1.215000033378601
    frame 264: 0.9841499924659729 -> 1.093500018119812
    frame 265: 0.8857349753379822 -> 0.9841499924659729
    frame 266: 0.7971615195274353 -> 0.8857349753379822
    frame 267: 0.7174453735351562 -> 0.7971615195274353
    frame 268: 0.6457008123397827 -> 0.7174453735351562
    (... 159 more)

  radarState.leadOne.vLeadK (963 diffs)
    frame 17: 30.48623275756836 -> 30.47705078125
    frame 18: 30.49445152282715 -> 30.48715591430664
    frame 19: 30.498546600341797 -> 30.489282608032227
    frame 20: 30.502378463745117 -> 30.50537109375
    frame 21: 30.502805709838867 -> 30.513587951660156
    frame 22: 30.506027221679688 -> 30.52056884765625
    frame 23: 30.510555267333984 -> 30.517702102661133
    frame 24: 30.520742416381836 -> 30.54315948486328
    frame 25: 30.53558349609375 -> 30.56039810180664
    frame 26: 30.547239303588867 -> 30.58368492126465
    (... 953 more)

  radarState.leadTwo.aLeadK (974 diffs)
    frame 17: 0.03307333588600159 -> 0.018868228420615196
    frame 18: 0.04250093176960945 -> 0.03212708234786987
    frame 19: 0.04532938078045845 -> 0.03255578503012657
    frame 20: 0.04757510870695114 -> 0.05466168373823166
    frame 21: 0.044773854315280914 -> 0.06348730623722076
    frame 22: 0.04618648812174797 -> 0.06955883651971817
    frame 23: 0.04937124252319336 -> 0.05970412865281105
    frame 24: 0.060453206300735474 -> 0.09425348043441772
    frame 25: 0.07742370665073395 -> 0.11312757432460785
    frame 26: 0.08860228210687637 -> 0.1405457705259323
    (... 964 more)

  radarState.leadTwo.aLeadTau (169 diffs)
    frame 107: 1.350000023841858 -> 1.5
    frame 249: 1.350000023841858 -> 1.5
    frame 261: 1.350000023841858 -> 1.5
    frame 262: 1.215000033378601 -> 1.350000023841858
    frame 263: 1.093500018119812 -> 1.215000033378601
    frame 264: 0.9841499924659729 -> 1.093500018119812
    frame 265: 0.8857349753379822 -> 0.9841499924659729
    frame 266: 0.7971615195274353 -> 0.8857349753379822
    frame 267: 0.7174453735351562 -> 0.7971615195274353
    frame 268: 0.6457008123397827 -> 0.7174453735351562
    (... 159 more)

  radarState.leadTwo.vLeadK (973 diffs)
    frame 17: 30.48623275756836 -> 30.47705078125
    frame 18: 30.49445152282715 -> 30.48715591430664
    frame 19: 30.498546600341797 -> 30.489282608032227
    frame 20: 30.502378463745117 -> 30.50537109375
    frame 21: 30.502805709838867 -> 30.513587951660156
    frame 22: 30.506027221679688 -> 30.52056884765625
    frame 23: 30.510555267333984 -> 30.517702102661133
    frame 24: 30.520742416381836 -> 30.54315948486328
    frame 25: 30.53558349609375 -> 30.56039810180664
    frame 26: 30.547239303588867 -> 30.58368492126465
    (... 963 more)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant