mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-07-24 04:52:13 +08:00
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
|
|
|
constructiond: work-zone detector for Speed Limit Assist.
|
|
|
|
Samples the road camera at ~2 Hz and looks for work-zone orange (barrels, drums,
|
|
diamond signs) in the NV12 chroma plane. Sunlit yellow lane paint renders with
|
|
nearly identical hue to barrel orange on this camera, but its red chroma (V)
|
|
saturates below ~168 while retroreflective barrel orange reaches 170-190, so the
|
|
V floor is the load-bearing threshold — do not lower it without re-running the
|
|
paint/barrel separation sweep on real footage.
|
|
"""
|
|
from collections import deque
|
|
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
import cereal.messaging as messaging
|
|
from cereal import custom
|
|
from msgq.visionipc import VisionIpcClient, VisionStreamType
|
|
from openpilot.common.swaglog import cloudlog
|
|
|
|
State = custom.IQConstructionZone.State
|
|
|
|
ANALYSIS_PERIOD = 0.5
|
|
|
|
# fractions of the chroma plane; excludes sky and hood
|
|
ROI_TOP, ROI_BOTTOM = 0.40, 0.94
|
|
ROI_LEFT, ROI_RIGHT = 0.04, 0.96
|
|
|
|
V_MIN = 170 # paint tops out ~168 in every DAYLIGHT condition sampled
|
|
CB_MIN = 16
|
|
HUE_LO, HUE_HI = 0.65, 2.0 # (V-128)/(128-U): yellow paint ~0.4, barrel orange ~0.7-1.5, red ~3.0
|
|
|
|
# Daylight-only gate: retroreflective amber markers (guardrail chevrons, object
|
|
# markers) blaze past V_MIN under headlights at night — 137px on one chevron on
|
|
# real footage — and there is no night work-zone ground truth to tune against.
|
|
# Night ROI mean luma measured ~36-38, validated daytime footage 84-98.
|
|
LUMA_MIN = 65.0
|
|
|
|
# hits only accumulate at highway-approach speeds: brightly lit lots (truck
|
|
# stops) can pass the luma gate at night with orange signage, and the clamp is
|
|
# meaningless below it anyway. An already-active zone still holds while slowed.
|
|
MIN_ENTER_SPEED = 13.4 # m/s (~30 mph)
|
|
|
|
HIT_FRAC = 3.0e-4
|
|
WASH_FRAC = 0.10 # more orange than this is scene lighting (sunset), not objects
|
|
ENTER_HITS = 3
|
|
ENTER_WINDOW = 10 # analyses (~5 s)
|
|
HOLD_SEC = 120.0 # barrel-free stretches inside a zone last minutes; hold through them
|
|
|
|
|
|
def orange_fraction(buf) -> tuple[float, float]:
|
|
"""Returns (hot-orange fraction, mean luma) of the road ROI."""
|
|
h, w, stride, uv_off = buf.height, buf.width, buf.stride, buf.uv_offset
|
|
ch, cw = h // 2, w // 2
|
|
uv = buf.data[uv_off:uv_off + ch * stride].reshape(ch, stride)
|
|
|
|
r0, r1 = int(ROI_TOP * ch), int(ROI_BOTTOM * ch)
|
|
c0, c1 = int(ROI_LEFT * cw), int(ROI_RIGHT * cw)
|
|
step = 2 if cw > 700 else 1
|
|
|
|
u = uv[r0:r1:step, 2 * c0:2 * c1:2 * step].astype(np.int16)
|
|
v = uv[r0:r1:step, 2 * c0 + 1:2 * c1:2 * step].astype(np.int16)
|
|
|
|
cb = 128 - u
|
|
cr = v - 128
|
|
mask = (v >= V_MIN) & (cb >= CB_MIN) & (cr >= HUE_LO * cb) & (cr <= HUE_HI * cb)
|
|
|
|
y_plane = buf.data[:h * stride].reshape(h, stride)
|
|
luma = float(y_plane[2 * r0:2 * r1:4, 2 * c0:2 * c1:8].mean())
|
|
|
|
return float(mask.mean()), luma
|
|
|
|
|
|
class ConstructionZoneDetector:
|
|
def __init__(self):
|
|
self.recent = deque(maxlen=ENTER_WINDOW)
|
|
self.last_hit_t: float | None = None
|
|
self.active = False
|
|
self.frac = 0.0
|
|
|
|
@property
|
|
def state(self):
|
|
if self.active:
|
|
return State.active
|
|
if any(self.recent):
|
|
return State.pending
|
|
return State.inactive
|
|
|
|
def seconds_since_hit(self, now: float) -> float:
|
|
if self.last_hit_t is None:
|
|
return -1.0
|
|
return now - self.last_hit_t
|
|
|
|
def update(self, frac: float, now: float, luma: float = 255.0, v_ego: float = 255.0) -> bool:
|
|
self.frac = frac
|
|
hit = (HIT_FRAC <= frac <= WASH_FRAC) and luma >= LUMA_MIN and v_ego >= MIN_ENTER_SPEED
|
|
self.recent.append(hit)
|
|
if hit:
|
|
self.last_hit_t = now
|
|
|
|
if self.active:
|
|
if self.last_hit_t is None or (now - self.last_hit_t) > HOLD_SEC:
|
|
self.active = False
|
|
self.recent.clear()
|
|
elif sum(self.recent) >= ENTER_HITS:
|
|
self.active = True
|
|
return self.active
|
|
|
|
|
|
def main():
|
|
pm = messaging.PubMaster(["iqConstructionZone"])
|
|
car_state_sock = messaging.sub_sock("carState", conflate=True)
|
|
detector = ConstructionZoneDetector()
|
|
v_ego = 0.0
|
|
|
|
vipc = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
|
|
while not vipc.connect(False):
|
|
time.sleep(0.5)
|
|
cloudlog.info("constructiond: connected to road camera stream")
|
|
|
|
while True:
|
|
t0 = time.monotonic()
|
|
buf = vipc.recv(200)
|
|
if buf is None:
|
|
# no publish on camera stall: SLC sees us stale and releases the clamp
|
|
continue
|
|
|
|
cs = messaging.recv_one_or_none(car_state_sock)
|
|
if cs is not None:
|
|
v_ego = cs.carState.vEgo
|
|
|
|
frac, luma = orange_fraction(buf)
|
|
detector.update(frac, t0, luma, v_ego)
|
|
|
|
msg = messaging.new_message("iqConstructionZone")
|
|
msg.valid = True
|
|
cz = msg.iqConstructionZone
|
|
cz.state = detector.state
|
|
cz.active = detector.active
|
|
cz.orangeFraction = frac
|
|
cz.secondsSinceHit = detector.seconds_since_hit(t0)
|
|
pm.send("iqConstructionZone", msg)
|
|
|
|
time.sleep(max(0.0, ANALYSIS_PERIOD - (time.monotonic() - t0)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|