openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
9.2 KiB

3 months ago
#!/usr/bin/env python3
import numpy as np
from collections import deque
from functools import partial
3 months ago
import cereal.messaging as messaging
1 month ago
from cereal import car, log
1 month ago
from cereal.services import SERVICE_LIST
3 months ago
from openpilot.common.params import Params
1 month ago
from openpilot.common.realtime import config_realtime_process, DT_CTRL
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
3 months ago
MIN_LAG_VEL = 20.0
3 months ago
MAX_SANE_LAG = 3.0
MIN_ABS_YAW_RATE_DEG = 1
2 months ago
MOVING_CORR_WINDOW = 300.0
MIN_OKAY_WINDOW = 25.0
MIN_NCC = 0.95
1 month ago
class Points:
def __init__(self, num_points):
self.times = deque(maxlen=num_points)
self.okay = deque(maxlen=num_points)
self.desired = deque(maxlen=num_points)
self.actual = deque(maxlen=num_points)
1 month ago
@property
def num_points(self):
return len(self.desired)
3 months ago
1 month ago
@property
def num_okay(self):
return np.count_nonzero(self.okay)
3 months ago
1 month ago
def update(self, t, desired, actual, okay):
self.times.append(t)
self.okay.append(okay)
self.desired.append(desired)
self.actual.append(actual)
def get(self):
return np.array(self.times), np.array(self.desired), np.array(self.actual), np.array(self.okay)
class BlockAverage:
1 month ago
def __init__(self, num_blocks, block_size, valid_blocks, initial_value):
1 month ago
self.num_blocks = num_blocks
self.block_size = block_size
self.block_idx = 0
self.idx = 0
self.values = np.tile(initial_value, (num_blocks, 1))
1 month ago
self.valid_blocks = valid_blocks
1 month ago
def update(self, value):
self.values[self.block_idx] = (self.idx * self.values[self.block_idx] + (self.block_size - self.idx) * value) / self.block_size
self.idx = (self.idx + 1) % self.block_size
if self.idx == 0:
self.block_idx = (self.block_idx + 1) % self.num_blocks
self.valid_blocks = min(self.valid_blocks + 1, self.num_blocks)
def get(self):
valid_block_idx = [i for i in range(self.valid_blocks) if i != self.block_idx]
if not valid_block_idx:
return None
return np.mean(self.values[valid_block_idx], axis=0)
class LagEstimator:
1 month ago
def __init__(self, CP, dt, block_count=10, block_size=100, window_sec=300.0, okay_window_sec=30.0, min_vego=15, min_yr=np.radians(1), min_ncc=0.95):
3 months ago
self.dt = dt
1 month ago
self.window_sec = window_sec
self.okay_window_sec = okay_window_sec
1 month ago
self.initial_lag = CP.steerActuatorDelay + 0.2
self.block_size = block_size
self.block_count = block_count
1 month ago
self.min_vego = min_vego
self.min_yr = min_yr
self.min_ncc = min_ncc
self.t = 0
self.lat_active = False
self.steering_pressed = False
self.desired_curvature = 0
self.v_ego = 0
self.yaw_rate = 0
3 months ago
self.calibrator = PoseCalibrator()
1 month ago
self.reset(self.initial_lag, 0)
def reset(self, initial_lag, valid_blocks):
window_len = int(self.window_sec / self.dt)
self.points = Points(window_len)
self.block_avg = BlockAverage(self.block_count, self.block_size, valid_blocks, initial_lag)
self.lag = initial_lag
1 month ago
def get_msg(self, valid):
msg = messaging.new_message('liveActuatorDelay')
3 months ago
1 month ago
msg.valid = valid
3 months ago
1 month ago
liveDelay = msg.liveActuatorDelay
liveDelay.steerActuatorDelay = self.lag
liveDelay.isEstimated = self.block_avg.valid_blocks > 0
1 month ago
liveDelay.validBlocks = self.block_avg.valid_blocks
1 month ago
liveDelay.points = self.block_avg.values.tolist()
1 month ago
return msg
def handle_log(self, t, which, msg):
3 months ago
if which == "carControl":
1 month ago
self.lat_active = msg.latActive
3 months ago
elif which == "carState":
1 month ago
self.steering_pressed = msg.steeringPressed
self.v_ego = msg.vEgo
3 months ago
elif which == "controlsState":
1 month ago
self.desired_curvature = msg.desiredCurvature
elif which == "liveCalibration":
self.calibrator.feed_live_calib(msg)
elif which == "livePose":
device_pose = Pose.from_live_pose(msg)
calibrated_pose = self.calibrator.build_calibrated_pose(device_pose)
1 month ago
self.yaw_rate = calibrated_pose.angular_velocity.z
self.t = t
1 month ago
def points_valid(self):
1 month ago
return self.points.num_okay >= int(self.okay_window_sec / self.dt)
3 months ago
1 month ago
def update_points(self):
okay = self.lat_active and not self.steering_pressed and self.v_ego > self.min_vego and np.abs(self.yaw_rate) >= self.min_yr
la_desired = self.desired_curvature * self.v_ego * self.v_ego
la_actual_pose = self.yaw_rate * self.v_ego
3 months ago
1 month ago
self.points.update(self.t, la_desired, la_actual_pose, okay)
1 month ago
if not okay or not self.points_valid():
1 month ago
return
3 months ago
1 month ago
times, desired, actual, okay = self.points.get()
times_interp = np.arange(times[-1] - self.window_sec, times[-1], DT_CTRL)
desired_interp = np.interp(times_interp, times, desired)
actual_interp = np.interp(times_interp, times, actual)
okay_interp = np.interp(times_interp, times, okay).astype(bool)
delay, corr = self.actuator_delay(desired_interp, actual_interp, okay_interp, DT_CTRL)
if corr < self.min_ncc:
return
1 month ago
self.block_avg.update(delay)
if (new_lag := self.block_avg.get()) is not None:
1 month ago
self.lag = float(new_lag.item())
3 months ago
def correlation_lags(self, sig_len, dt):
return np.arange(0, sig_len) * dt
def actuator_delay(self, expected_sig, actual_sig, mask, dt, max_lag=1.):
"""
References:
D. Padfield. "Masked FFT registration". In Proc. Computer Vision and
Pattern Recognition, pp. 2918-2925 (2010).
:DOI:`10.1109/CVPR.2010.5540032`
"""
eps = np.finfo(np.float64).eps
expected_sig = np.asarray(expected_sig, dtype=np.float64)
actual_sig = np.asarray(actual_sig, dtype=np.float64)
expected_sig[~mask] = 0.0
actual_sig[~mask] = 0.0
rotated_expected_sig = expected_sig[::-1]
rotated_mask = mask[::-1]
n = len(expected_sig) + len(actual_sig) - 1
fft = partial(np.fft.fft, n=n)
actual_sig_fft = fft(actual_sig)
rotated_expected_sig_fft = fft(rotated_expected_sig)
actual_mask_fft = fft(mask.astype(np.float64))
rotated_mask_fft = fft(rotated_mask.astype(np.float64))
number_overlap_masked_samples = np.fft.ifft(rotated_mask_fft * actual_mask_fft).real
number_overlap_masked_samples[:] = np.round(number_overlap_masked_samples)
number_overlap_masked_samples[:] = np.fmax(number_overlap_masked_samples, eps)
masked_correlated_actual_fft = np.fft.ifft(rotated_mask_fft * actual_sig_fft).real
masked_correlated_expected_fft = np.fft.ifft(actual_mask_fft * rotated_expected_sig_fft).real
numerator = np.fft.ifft(rotated_expected_sig_fft * actual_sig_fft).real
numerator -= masked_correlated_actual_fft * masked_correlated_expected_fft / number_overlap_masked_samples
actual_squared_fft = fft(actual_sig ** 2)
actual_sig_denom = np.fft.ifft(rotated_mask_fft * actual_squared_fft).real
actual_sig_denom -= masked_correlated_actual_fft ** 2 / number_overlap_masked_samples
actual_sig_denom[:] = np.fmax(actual_sig_denom, 0.0)
rotated_expected_squared_fft = fft(rotated_expected_sig ** 2)
expected_sig_denom = np.fft.ifft(actual_mask_fft * rotated_expected_squared_fft).real
expected_sig_denom -= masked_correlated_expected_fft ** 2 / number_overlap_masked_samples
expected_sig_denom[:] = np.fmax(expected_sig_denom, 0.0)
denom = np.sqrt(actual_sig_denom * expected_sig_denom)
# zero-out samples with very small denominators
tol = 1e3 * eps * np.max(np.abs(denom), keepdims=True)
nonzero_indices = denom > tol
ncc = np.zeros_like(denom, dtype=np.float64)
ncc[nonzero_indices] = numerator[nonzero_indices] / denom[nonzero_indices]
np.clip(ncc, -1, 1, out=ncc)
max_lag_samples = int(max_lag / dt)
roi_ncc = ncc[len(expected_sig) - 1: len(expected_sig) - 1 + max_lag_samples]
max_corr_index = np.argmax(roi_ncc)
corr = roi_ncc[max_corr_index]
lag = max_corr_index * dt
return lag, corr
3 months ago
def main():
config_realtime_process([0, 1, 2, 3], 5)
pm = messaging.PubMaster(['liveActuatorDelay', 'alertDebug'])
1 month ago
sm = messaging.SubMaster(['livePose', 'liveCalibration', 'carControl', 'carState', 'controlsState'], poll='livePose')
3 months ago
params = Params()
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
1 month ago
estimator = LagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency)
3 months ago
1 month ago
lag_params = params.get("LiveLag")
1 month ago
if lag_params:
try:
with log.Event.from_bytes(lag_params) as msg:
lag_init = msg.liveActuatorDelay.steerActuatorDelay
valid_blocks = msg.liveActuatorDelay.validBlocks
estimator.reset(lag_init, valid_blocks)
except Exception:
print("Error reading cached LagParams")
3 months ago
while True:
sm.update()
if sm.all_checks():
2 months ago
for which in sorted(sm.updated.keys(), key=lambda x: sm.logMonoTime[x]):
3 months ago
if sm.updated[which]:
t = sm.logMonoTime[which] * 1e-9
estimator.handle_log(t, which, sm[which])
1 month ago
estimator.update_points()
3 months ago
if sm.frame % 25 == 0:
1 month ago
msg = estimator.get_msg(sm.all_checks())
3 months ago
alert_msg = messaging.new_message('alertDebug')
3 months ago
alert_msg.alertDebug.alertText1 = f"Lag estimate (fixed: {CP.steerActuatorDelay:.2f} s)"
2 months ago
alert_msg.alertDebug.alertText2 = f"{msg.liveActuatorDelay.steerActuatorDelay:.2f} s ({msg.liveActuatorDelay.isEstimated})"
3 months ago
pm.send('liveActuatorDelay', msg)
pm.send('alertDebug', alert_msg)
1 month ago
if msg.liveActuatorDelay.isEstimated: # TODO maybe to often once estimated
1 month ago
params.put_nonblocking("LiveLag", msg.to_bytes())
1 month ago
3 months ago
if __name__ == "__main__":
main()