radard.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python3
  2. import math
  3. from collections import deque
  4. from typing import Any
  5. import capnp
  6. from cereal import messaging, log, car
  7. from openpilot.common.numpy_fast import interp
  8. from openpilot.common.params import Params
  9. from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process
  10. from openpilot.common.swaglog import cloudlog
  11. from openpilot.common.simple_kalman import KF1D
  12. # Default lead acceleration decay set to 50% at 1s
  13. _LEAD_ACCEL_TAU = 1.5
  14. # radar tracks
  15. SPEED, ACCEL = 0, 1 # Kalman filter states enum
  16. # stationary qualification parameters
  17. V_EGO_STATIONARY = 4. # no stationary object flag below this speed
  18. RADAR_TO_CENTER = 2.7 # (deprecated) RADAR is ~ 2.7m ahead from center of car
  19. RADAR_TO_CAMERA = 1.52 # RADAR is ~ 1.5m ahead from center of mesh frame
  20. class KalmanParams:
  21. def __init__(self, dt: float):
  22. # Lead Kalman Filter params, calculating K from A, C, Q, R requires the control library.
  23. # hardcoding a lookup table to compute K for values of radar_ts between 0.01s and 0.2s
  24. assert dt > .01 and dt < .2, "Radar time step must be between .01s and 0.2s"
  25. self.A = [[1.0, dt], [0.0, 1.0]]
  26. self.C = [1.0, 0.0]
  27. #Q = np.matrix([[10., 0.0], [0.0, 100.]])
  28. #R = 1e3
  29. #K = np.matrix([[ 0.05705578], [ 0.03073241]])
  30. dts = [i * 0.01 for i in range(1, 21)]
  31. K0 = [0.12287673, 0.14556536, 0.16522756, 0.18281627, 0.1988689, 0.21372394,
  32. 0.22761098, 0.24069424, 0.253096, 0.26491023, 0.27621103, 0.28705801,
  33. 0.29750003, 0.30757767, 0.31732515, 0.32677158, 0.33594201, 0.34485814,
  34. 0.35353899, 0.36200124]
  35. K1 = [0.29666309, 0.29330885, 0.29042818, 0.28787125, 0.28555364, 0.28342219,
  36. 0.28144091, 0.27958406, 0.27783249, 0.27617149, 0.27458948, 0.27307714,
  37. 0.27162685, 0.27023228, 0.26888809, 0.26758976, 0.26633338, 0.26511557,
  38. 0.26393339, 0.26278425]
  39. self.K = [[interp(dt, dts, K0)], [interp(dt, dts, K1)]]
  40. class Track:
  41. def __init__(self, identifier: int, v_lead: float, kalman_params: KalmanParams):
  42. self.identifier = identifier
  43. self.cnt = 0
  44. self.aLeadTau = _LEAD_ACCEL_TAU
  45. self.K_A = kalman_params.A
  46. self.K_C = kalman_params.C
  47. self.K_K = kalman_params.K
  48. self.kf = KF1D([[v_lead], [0.0]], self.K_A, self.K_C, self.K_K)
  49. def update(self, d_rel: float, y_rel: float, v_rel: float, v_lead: float, measured: float):
  50. # relative values, copy
  51. self.dRel = d_rel # LONG_DIST
  52. self.yRel = y_rel # -LAT_DIST
  53. self.vRel = v_rel # REL_SPEED
  54. self.vLead = v_lead
  55. self.measured = measured # measured or estimate
  56. # computed velocity and accelerations
  57. if self.cnt > 0:
  58. self.kf.update(self.vLead)
  59. self.vLeadK = float(self.kf.x[SPEED][0])
  60. self.aLeadK = float(self.kf.x[ACCEL][0])
  61. # Learn if constant acceleration
  62. if abs(self.aLeadK) < 0.5:
  63. self.aLeadTau = _LEAD_ACCEL_TAU
  64. else:
  65. self.aLeadTau *= 0.9
  66. self.cnt += 1
  67. def get_key_for_cluster(self):
  68. # Weigh y higher since radar is inaccurate in this dimension
  69. return [self.dRel, self.yRel*2, self.vRel]
  70. def reset_a_lead(self, aLeadK: float, aLeadTau: float):
  71. self.kf = KF1D([[self.vLead], [aLeadK]], self.K_A, self.K_C, self.K_K)
  72. self.aLeadK = aLeadK
  73. self.aLeadTau = aLeadTau
  74. def get_RadarState(self, model_prob: float = 0.0):
  75. return {
  76. "dRel": float(self.dRel),
  77. "yRel": float(self.yRel),
  78. "vRel": float(self.vRel),
  79. "vLead": float(self.vLead),
  80. "vLeadK": float(self.vLeadK),
  81. "aLeadK": float(self.aLeadK),
  82. "aLeadTau": float(self.aLeadTau),
  83. "status": True,
  84. "fcw": self.is_potential_fcw(model_prob),
  85. "modelProb": model_prob,
  86. "radar": True,
  87. "radarTrackId": self.identifier,
  88. }
  89. def potential_low_speed_lead(self, v_ego: float):
  90. # stop for stuff in front of you and low speed, even without model confirmation
  91. # Radar points closer than 0.75, are almost always glitches on toyota radars
  92. return abs(self.yRel) < 1.0 and (v_ego < V_EGO_STATIONARY) and (0.75 < self.dRel < 25)
  93. def is_potential_fcw(self, model_prob: float):
  94. return model_prob > .9
  95. def __str__(self):
  96. ret = f"x: {self.dRel:4.1f} y: {self.yRel:4.1f} v: {self.vRel:4.1f} a: {self.aLeadK:4.1f}"
  97. return ret
  98. def laplacian_pdf(x: float, mu: float, b: float):
  99. b = max(b, 1e-4)
  100. return math.exp(-abs(x-mu)/b)
  101. def match_vision_to_track(v_ego: float, lead: capnp._DynamicStructReader, tracks: dict[int, Track]):
  102. offset_vision_dist = lead.x[0] - RADAR_TO_CAMERA
  103. def prob(c):
  104. prob_d = laplacian_pdf(c.dRel, offset_vision_dist, lead.xStd[0])
  105. prob_y = laplacian_pdf(c.yRel, -lead.y[0], lead.yStd[0])
  106. prob_v = laplacian_pdf(c.vRel + v_ego, lead.v[0], lead.vStd[0])
  107. # This isn't exactly right, but it's a good heuristic
  108. return prob_d * prob_y * prob_v
  109. track = max(tracks.values(), key=prob)
  110. # if no 'sane' match is found return -1
  111. # stationary radar points can be false positives
  112. dist_sane = abs(track.dRel - offset_vision_dist) < max([(offset_vision_dist)*.25, 5.0])
  113. vel_sane = (abs(track.vRel + v_ego - lead.v[0]) < 10) or (v_ego + track.vRel > 3)
  114. if dist_sane and vel_sane:
  115. return track
  116. else:
  117. return None
  118. def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: float, model_v_ego: float):
  119. lead_v_rel_pred = lead_msg.v[0] - model_v_ego
  120. return {
  121. "dRel": float(lead_msg.x[0] - RADAR_TO_CAMERA),
  122. "yRel": float(-lead_msg.y[0]),
  123. "vRel": float(lead_v_rel_pred),
  124. "vLead": float(v_ego + lead_v_rel_pred),
  125. "vLeadK": float(v_ego + lead_v_rel_pred),
  126. "aLeadK": 0.0,
  127. "aLeadTau": 0.3,
  128. "fcw": False,
  129. "modelProb": float(lead_msg.prob),
  130. "status": True,
  131. "radar": False,
  132. "radarTrackId": -1,
  133. }
  134. def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capnp._DynamicStructReader,
  135. model_v_ego: float, low_speed_override: bool = True) -> dict[str, Any]:
  136. # Determine leads, this is where the essential logic happens
  137. if len(tracks) > 0 and ready and lead_msg.prob > .5:
  138. track = match_vision_to_track(v_ego, lead_msg, tracks)
  139. else:
  140. track = None
  141. lead_dict = {'status': False}
  142. if track is not None:
  143. lead_dict = track.get_RadarState(lead_msg.prob)
  144. elif (track is None) and ready and (lead_msg.prob > .5):
  145. lead_dict = get_RadarState_from_vision(lead_msg, v_ego, model_v_ego)
  146. if low_speed_override:
  147. low_speed_tracks = [c for c in tracks.values() if c.potential_low_speed_lead(v_ego)]
  148. if len(low_speed_tracks) > 0:
  149. closest_track = min(low_speed_tracks, key=lambda c: c.dRel)
  150. # Only choose new track if it is actually closer than the previous one
  151. if (not lead_dict['status']) or (closest_track.dRel < lead_dict['dRel']):
  152. lead_dict = closest_track.get_RadarState()
  153. return lead_dict
  154. class RadarD:
  155. def __init__(self, delay: float = 0.0):
  156. self.current_time = 0.0
  157. self.tracks: dict[int, Track] = {}
  158. self.kalman_params = KalmanParams(DT_MDL)
  159. self.v_ego = 0.0
  160. self.v_ego_hist = deque([0.0], maxlen=int(round(delay / DT_MDL))+1)
  161. self.last_v_ego_frame = -1
  162. self.radar_state: capnp._DynamicStructBuilder | None = None
  163. self.radar_state_valid = False
  164. self.ready = False
  165. def update(self, sm: messaging.SubMaster, rr: car.RadarData):
  166. self.ready = sm.seen['modelV2']
  167. self.current_time = 1e-9*max(sm.logMonoTime.values())
  168. if sm.recv_frame['carState'] != self.last_v_ego_frame:
  169. self.v_ego = sm['carState'].vEgo
  170. self.v_ego_hist.append(self.v_ego)
  171. self.last_v_ego_frame = sm.recv_frame['carState']
  172. ar_pts = {}
  173. for pt in rr.points:
  174. ar_pts[pt.trackId] = [pt.dRel, pt.yRel, pt.vRel, pt.measured]
  175. # *** remove missing points from meta data ***
  176. for ids in list(self.tracks.keys()):
  177. if ids not in ar_pts:
  178. self.tracks.pop(ids, None)
  179. # *** compute the tracks ***
  180. for ids in ar_pts:
  181. rpt = ar_pts[ids]
  182. # align v_ego by a fixed time to align it with the radar measurement
  183. v_lead = rpt[2] + self.v_ego_hist[0]
  184. # create the track if it doesn't exist or it's a new track
  185. if ids not in self.tracks:
  186. self.tracks[ids] = Track(ids, v_lead, self.kalman_params)
  187. self.tracks[ids].update(rpt[0], rpt[1], rpt[2], v_lead, rpt[3])
  188. # *** publish radarState ***
  189. self.radar_state_valid = sm.all_checks() and len(rr.errors) == 0
  190. self.radar_state = log.RadarState.new_message()
  191. self.radar_state.mdMonoTime = sm.logMonoTime['modelV2']
  192. self.radar_state.radarErrors = list(rr.errors)
  193. self.radar_state.carStateMonoTime = sm.logMonoTime['carState']
  194. if len(sm['modelV2'].velocity.x):
  195. model_v_ego = sm['modelV2'].velocity.x[0]
  196. else:
  197. model_v_ego = self.v_ego
  198. leads_v3 = sm['modelV2'].leadsV3
  199. if len(leads_v3) > 1:
  200. self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, low_speed_override=True)
  201. self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, low_speed_override=False)
  202. def publish(self, pm: messaging.PubMaster):
  203. assert self.radar_state is not None
  204. radar_msg = messaging.new_message("radarState")
  205. radar_msg.valid = self.radar_state_valid
  206. radar_msg.radarState = self.radar_state
  207. pm.send("radarState", radar_msg)
  208. # fuses camera and radar data for best lead detection
  209. def main() -> None:
  210. config_realtime_process(5, Priority.CTRL_LOW)
  211. # wait for stats about the car to come in from controls
  212. cloudlog.info("radard is waiting for CarParams")
  213. CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams)
  214. cloudlog.info("radard got CarParams")
  215. # *** setup messaging
  216. sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2')
  217. pm = messaging.PubMaster(['radarState'])
  218. RD = RadarD(CP.radarDelay)
  219. while 1:
  220. sm.update()
  221. RD.update(sm, sm['liveTracks'])
  222. RD.publish(pm)
  223. if __name__ == "__main__":
  224. main()