mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 17:13:54 +08:00
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
"""Frozen StarPilot lead tracking/following detector over validated current inputs.
|
|
|
|
This module has no messaging or control authority. Its caller supplies freshness for
|
|
current radar/model/car events and the selected longitudinal headway owner.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
from openpilot.common.realtime import DT_MDL
|
|
from openpilot.starpilot.model_geometry import STANDSTILL_RELEASE_SPEED_MPS, normalized_origin, standstill_clear_horizon
|
|
|
|
|
|
STOP_DISTANCE_M = 6.0
|
|
FILTER_TIME_S = 0.5
|
|
FILTER_THRESHOLD = 1.0 - 1.0 / math.e
|
|
CONTINUITY_THRESHOLD = FILTER_THRESHOLD * 0.6
|
|
RADARLESS_HOLD_S = 0.45
|
|
MAX_FRAME_GAP_S = 2 * DT_MDL
|
|
SOURCE_MAX_AGE_S = 0.25
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LeadObservation:
|
|
raw_present: bool | None
|
|
tracked: bool | None
|
|
following: bool | None
|
|
raw_radar: bool | None
|
|
|
|
|
|
def _number(value: object, low: float, high: float) -> float | None:
|
|
if isinstance(value, bool) or not isinstance(value, (float, int)):
|
|
return None
|
|
try:
|
|
number = float(value)
|
|
except (OverflowError, ValueError):
|
|
return None
|
|
return number if math.isfinite(number) and low <= number <= high else None
|
|
|
|
|
|
def _get(obj: object, field: str) -> object | None:
|
|
try:
|
|
return getattr(obj, field)
|
|
except (AttributeError, TypeError, ValueError, RuntimeError):
|
|
return None
|
|
|
|
|
|
def _smoothstep(value: float, start: float, end: float) -> float:
|
|
factor = min(1.0, max(0.0, (value - start) / max(end - start, 1e-3)))
|
|
return factor * factor * (3.0 - 2.0 * factor)
|
|
|
|
|
|
def _path_at_distance(model: object, distance_m: float) -> tuple[float, float] | None:
|
|
position = _get(model, 'position')
|
|
try:
|
|
xs = normalized_origin(tuple(position.x))
|
|
ys = tuple(position.y)
|
|
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
return None
|
|
if len(xs) != 33 or len(ys) != 33:
|
|
return None
|
|
distances = tuple(_number(x, 0.0, 500.0) for x in xs)
|
|
offsets = tuple(_number(y, -50.0, 50.0) for y in ys)
|
|
if any(value is None for value in distances) or any(value is None for value in offsets):
|
|
return None
|
|
clean_xs = tuple(value for value in distances if value is not None)
|
|
clean_ys = tuple(value for value in offsets if value is not None)
|
|
if any(b < a for a, b in zip(clean_xs, clean_xs[1:], strict=False)):
|
|
return None
|
|
return clean_xs[-1], float(np.interp(distance_m, clean_xs, clean_ys))
|
|
|
|
|
|
def _should_track(present: bool, distance: float, model_length: float, speed: float, lead_speed: float, radar: bool) -> bool:
|
|
if not present:
|
|
return False
|
|
tracking_buffer = max(STOP_DISTANCE_M, 4.0)
|
|
model_limit = model_length + tracking_buffer
|
|
if radar:
|
|
return distance < model_limit
|
|
closing = max(0.0, speed - lead_speed)
|
|
gap = 1.75 + min(closing * 0.20, 2.50)
|
|
vision_limit = max(25.0, speed * gap + tracking_buffer)
|
|
return distance < min(model_limit, vision_limit)
|
|
|
|
|
|
def _should_hold_vision(present: bool, distance: float, model_length: float, speed: float, model_prob: float, y_rel: float, path_y: float, radar: bool) -> bool:
|
|
if not present or radar or model_prob < 0.70:
|
|
return False
|
|
lateral = abs(y_rel + path_y)
|
|
if lateral > 1.6:
|
|
return False
|
|
tracking_buffer = max(STOP_DISTANCE_M, 4.0)
|
|
model_limit = model_length + tracking_buffer
|
|
exit_limit = max(25.0, speed * 2.30 + tracking_buffer)
|
|
if distance < min(model_limit, exit_limit):
|
|
return True
|
|
if model_prob < 0.95 or lateral > 1.1:
|
|
return False
|
|
speed_factor = 1.0 - _smoothstep(speed, 20.0, 25.0)
|
|
continuity_gap = 2.30 + 0.55 * speed_factor
|
|
continuity_limit = max(25.0, speed * continuity_gap + tracking_buffer)
|
|
return distance < continuity_limit
|
|
|
|
|
|
def _radarless_follow_window(speed: float, distance: float, lead_speed: float, headway: float, radar: bool, lead_brake: float, model_prob: float) -> bool:
|
|
if radar or headway <= 0.0 or speed < 22.0 or model_prob < 0.70 or lead_brake > 0.35:
|
|
return False
|
|
if abs(speed - lead_speed) > 2.0:
|
|
return False
|
|
actual_gap = distance / max(speed, 1e-3)
|
|
min_gap = max(0.95, headway - 0.35)
|
|
max_gap = headway + 0.90
|
|
return min_gap <= actual_gap <= max_gap
|
|
|
|
|
|
class LeadDetector:
|
|
"""One detector per drive. Unknown/dead sources reset temporal consensus."""
|
|
|
|
def __init__(self):
|
|
self.reset()
|
|
|
|
def reset(self) -> None:
|
|
self.filter_value = 0.0
|
|
self.tracked = False
|
|
self.radarless_hold_until_s = 0.0
|
|
self.last_observed_mono_s: float | None = None
|
|
|
|
def step(
|
|
self,
|
|
radar_lead: object | None,
|
|
model: object | None,
|
|
*,
|
|
speed_mps: float | None,
|
|
t_follow_s: float | None,
|
|
standstill: bool | None,
|
|
observed_mono_s: float,
|
|
now_mono_s: float,
|
|
radar_fresh: bool,
|
|
model_fresh: bool,
|
|
car_fresh: bool,
|
|
headway_fresh: bool,
|
|
) -> LeadObservation:
|
|
present = _get(radar_lead, 'present') if radar_fresh else None
|
|
radar = _get(radar_lead, 'radar') if radar_fresh else None
|
|
raw_present = present if type(present) is bool else None
|
|
raw_radar = radar if type(radar) is bool else None
|
|
unknown = LeadObservation(raw_present, None, None, raw_radar)
|
|
speed = _number(speed_mps, 0.0, 80.0)
|
|
headway = _number(t_follow_s, 0.0, 5.0)
|
|
observed = _number(observed_mono_s, 0.0, 1e12)
|
|
now = _number(now_mono_s, 0.0, 1e12)
|
|
distance = _number(_get(radar_lead, 'dRel'), 0.0, 500.0)
|
|
lead_speed = _number(_get(radar_lead, 'vLead'), -30.0, 100.0)
|
|
lead_brake = _number(_get(radar_lead, 'aLeadK'), -20.0, 20.0)
|
|
prob = _number(_get(radar_lead, 'modelProb'), 0.0, 1.0)
|
|
y_rel = _number(_get(radar_lead, 'yRel'), -20.0, 20.0)
|
|
path = _path_at_distance(model, distance) if model_fresh and distance is not None else None
|
|
if path is None and model_fresh and standstill is True and speed is not None and speed <= STANDSTILL_RELEASE_SPEED_MPS:
|
|
# Only retain a stopped track; these coordinates never enter interpolation.
|
|
horizon = standstill_clear_horizon(model)
|
|
try:
|
|
offsets = tuple(_number(value, -50.0, 50.0) for value in model.position.y)
|
|
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
offsets = ()
|
|
if horizon is not None and len(offsets) == 33 and all(value is not None for value in offsets):
|
|
path = horizon, 0.0
|
|
if (
|
|
not radar_fresh
|
|
or not model_fresh
|
|
or not car_fresh
|
|
or not headway_fresh
|
|
or raw_present is None
|
|
or raw_radar is None
|
|
or speed is None
|
|
or headway is None
|
|
or type(standstill) is not bool
|
|
or observed is None
|
|
or now is None
|
|
or not 0.0 <= now - observed <= SOURCE_MAX_AGE_S
|
|
or path is None
|
|
or None in (distance, lead_speed, lead_brake, prob, y_rel)
|
|
):
|
|
self.reset()
|
|
return unknown
|
|
assert distance is not None and lead_speed is not None and lead_brake is not None and prob is not None and y_rel is not None
|
|
model_length, path_y = path
|
|
if self.last_observed_mono_s is not None:
|
|
interval = observed - self.last_observed_mono_s
|
|
if interval <= 0.0:
|
|
return unknown # One source event cannot advance the model-tick filter twice.
|
|
if interval > MAX_FRAME_GAP_S:
|
|
self.reset()
|
|
return unknown
|
|
self.last_observed_mono_s = observed
|
|
if not standstill:
|
|
candidate = _should_track(raw_present, distance, model_length, speed, lead_speed, raw_radar)
|
|
continuity = self.tracked or self.filter_value >= CONTINUITY_THRESHOLD
|
|
if not candidate and continuity:
|
|
candidate = _should_hold_vision(raw_present, distance, model_length, speed, prob, y_rel, path_y, raw_radar)
|
|
window = raw_present and _radarless_follow_window(speed, distance, lead_speed, max(headway, 1.45), raw_radar, max(0.0, -lead_brake), prob)
|
|
if window and (candidate or self.tracked or self.filter_value >= CONTINUITY_THRESHOLD):
|
|
self.radarless_hold_until_s = observed + RADARLESS_HOLD_S
|
|
elif raw_radar or not raw_present:
|
|
self.radarless_hold_until_s = 0.0
|
|
if not candidate and window and observed < self.radarless_hold_until_s:
|
|
candidate = True
|
|
alpha = DT_MDL / (FILTER_TIME_S + DT_MDL)
|
|
self.filter_value = (1.0 - alpha) * self.filter_value + alpha * float(candidate)
|
|
self.tracked = self.filter_value >= FILTER_THRESHOLD
|
|
# Retain the filter's brief following state after leadOne.present becomes false.
|
|
following = self.tracked and distance < (headway * 2.0) * speed
|
|
return LeadObservation(raw_present, self.tracked, following, raw_radar)
|