文章目录
一、前言
我去https://accidentbench.github.io/找了3个车辆碰撞的视频用于测试。第二节的思路是考虑到碰撞的核心特征就是角速度突变+急减速,然后我试了一下代码,通过了2个视频的车辆碰撞案例(ACCIDENT Project Page_1015337455.mp4和ACCIDENT Project Page_3087282190.mp4),但是第3个视频(ACCIDENT Project Page_3213711612.mp4)中大量跟车的现象,不停出现误检,实际上第1个视频也出现了误检,但是第3个视频的误检相当严重。
第三节代码解决了第3个视频的误检问题,同时第1和第2个视频也检测出了正确的碰撞,但是第3个视频的碰撞没检测出来(远处过来的一个车yolo没检测出来),然后第2个视频里面虽然检测出碰撞,但是那是车碰障碍物,但是显示的是车碰车。
二、车辆碰撞1——角速度突变 + 急减速
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
collision_detector.py
工程化第一版:车辆/障碍物碰撞风险检测
--------------------------------------
核心改进:
1. 保留 YOLO + ByteTrack 跟踪
2. 使用 bbox 几何关系 + 运动学验证(角速度突变 + 急减速)区分
自然行驶与碰撞后的异常状态
3. 显示优化:移除连线,只在风险状态显示关键信息
4. 修复状态机冷却逻辑,增加类型安全处理
"""
import argparse
import math
import time
from dataclasses import dataclass, field
from collections import defaultdict, deque
from typing import Dict, List, Tuple, Optional
import cv2
import numpy as np
from ultralytics import YOLO
# ============================================================
# 类别配置
# ============================================================
DEFAULT_VEHICLE_NAMES = {
"car", "truck", "bus", "motorcycle", "motorbike", "van"
}
DEFAULT_OBSTACLE_NAMES = {
"barrier", "guardrail", "road_obstacle", "obstacle",
"cone", "traffic_cone", "roadblock", "bollard"
}
# ============================================================
# 数据结构
# ============================================================
@dataclass
class Detection:
track_id: int
cls_id: int
name: str
conf: float
box: np.ndarray # x1,y1,x2,y2
mask: Optional[np.ndarray] = None
@property
def x1(self):
return float(self.box[0])
@property
def y1(self):
return float(self.box[1])
@property
def x2(self):
return float(self.box[2])
@property
def y2(self):
return float(self.box[3])
@property
def w(self):
return max(1.0, self.x2 - self.x1)
@property
def h(self):
return max(1.0, self.y2 - self.y1)
@property
def center(self):
return np.array([(self.x1 + self.x2) * 0.5,
(self.y1 + self.y2) * 0.5], dtype=np.float32)
@property
def bottom_center(self):
# 对道路目标而言,比 bbox 中心更接近真实接地点
return np.array([(self.x1 + self.x2) * 0.5,
self.y2], dtype=np.float32)
@property
def bottom_left(self):
return np.array([self.x1, self.y2], dtype=np.float32)
@property
def bottom_right(self):
return np.array([self.x2, self.y2], dtype=np.float32)
@property
def is_vehicle(self):
return str(self.name).lower() in DEFAULT_VEHICLE_NAMES
@property
def is_obstacle(self):
return str(self.name).lower() in DEFAULT_OBSTACLE_NAMES
@dataclass
class TrackState:
track_id: int
name: str
points: deque = field(default_factory=lambda: deque(maxlen=20))
boxes: deque = field(default_factory=lambda: deque(maxlen=20))
timestamps: deque = field(default_factory=lambda: deque(maxlen=20))
velocities: deque = field(default_factory=lambda: deque(maxlen=10)) # 存储速度向量
missed: int = 0
def update(self, det: Detection, t: float):
self.points.append(det.bottom_center.copy())
self.boxes.append(det.box.copy())
self.timestamps.append(t)
self.missed = 0
# 计算速度向量(如果有足够历史)
if len(self.points) >= 4:
vel = self.velocity
self.velocities.append(vel.copy())
@property
def velocity(self):
"""
鲁棒的图像速度估计。
不使用相邻两帧差分,避免检测抖动导致速度爆炸。
"""
if len(self.points) < 4:
return np.zeros(2, dtype=np.float32)
n = min(8, len(self.points))
p = np.asarray(list(self.points)[-n:], dtype=np.float32)
ts = np.asarray(list(self.timestamps)[-n:], dtype=np.float64)
dt = ts - ts[0]
if dt[-1] <= 1e-6:
return np.zeros(2, dtype=np.float32)
vx = np.polyfit(dt, p[:, 0], 1)[0]
vy = np.polyfit(dt, p[:, 1], 1)[0]
return np.array([vx, vy], dtype=np.float32)
@property
def acceleration(self):
if len(self.points) < 6:
return np.zeros(2, dtype=np.float32)
n = min(10, len(self.points))
p = np.asarray(list(self.points)[-n:], dtype=np.float32)
ts = np.asarray(list(self.timestamps)[-n:], dtype=np.float64)
v = []
for i in range(1, len(p)):
dt = ts[i] - ts[i - 1]
if dt > 1e-6:
v.append((p[i] - p[i - 1]) / dt)
if len(v) < 3:
return np.zeros(2, dtype=np.float32)
v = np.asarray(v)
dt = np.arange(len(v), dtype=np.float32)
ax = np.polyfit(dt, v[:, 0], 1)[0]
ay = np.polyfit(dt, v[:, 1], 1)[0]
return np.array([ax, ay], dtype=np.float32)
@property
def angular_speed(self):
"""
最近两帧速度方向变化率(弧度/秒)。
用于检测碰撞引起的甩头、急转等异常方向变化。
"""
if len(self.velocities) < 2:
return 0.0
v1 = self.velocities[-2]
v2 = self.velocities[-1]
# 使用对应的时间戳
if len(self.timestamps) < 2:
return 0.0
dt = self.timestamps[-1] - self.timestamps[-2]
if dt < 1e-3:
return 0.0
angle1 = math.atan2(v1[1], v1[0])
angle2 = math.atan2(v2[1], v2[0])
dangle = math.atan2(math.sin(angle2 - angle1), math.cos(angle2 - angle1))
return abs(dangle) / dt
@property
def longitudinal_decel(self):
"""
沿运动方向的减速度(正值表示减速,单位:像素/秒²)。
用于检测碰撞后速度骤降。
"""
if len(self.velocities) < 2:
return 0.0
v1 = self.velocities[-2]
v2 = self.velocities[-1]
if len(self.timestamps) < 2:
return 0.0
dt = self.timestamps[-1] - self.timestamps[-2]
if dt < 1e-3:
return 0.0
norm_v1 = float(np.linalg.norm(v1))
if norm_v1 < 1e-3:
return 0.0
unit_v1 = v1 / norm_v1
delta_v = v2 - v1
decel = -float(np.dot(delta_v, unit_v1)) / dt
return max(0.0, decel)
@property
def last_box(self):
return self.boxes[-1] if self.boxes else None
@property
def last_point(self):
return self.points[-1] if self.points else None
@dataclass
class PairState:
approaching_count: int = 0
warning_count: int = 0
critical_count: int = 0
collision_count: int = 0
separating_count: int = 0
cooldown: int = 0
last_risk: float = 0.0
last_ttc: float = float("inf")
last_gap: float = float("inf")
state: str = "NORMAL"
# ============================================================
# 几何工具
# ============================================================
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def sigmoid(x):
x = clamp(float(x), -30.0, 30.0)
return 1.0 / (1.0 + math.exp(-x))
def bbox_iou(a, b):
x1 = max(a[0], b[0])
y1 = max(a[1], b[1])
x2 = min(a[2], b[2])
y2 = min(a[3], b[3])
iw = max(0.0, x2 - x1)
ih = max(0.0, y2 - y1)
inter = iw * ih
aa = max(0.0, a[2] - a[0]) * max(0.0, a[3] - a[1])
ab = max(0.0, b[2] - b[0]) * max(0.0, b[3] - b[1])
union = aa + ab - inter
return inter / union if union > 1e-6 else 0.0
def bbox_gap(a, b):
"""
计算两个 bbox 的边缘间隙。
比中心点距离更适合碰撞判断。
"""
dx = max(a[0] - b[2], b[0] - a[2], 0.0)
dy = max(a[1] - b[3], b[1] - a[3], 0.0)
return math.hypot(dx, dy)
def bottom_gap(a: Detection, b: Detection):
"""
对道路目标使用底部几何关系。
返回:
x_gap: 底部横向间隙
y_gap: 底部纵向间隙
"""
x_gap = max(a.x1 - b.x2, b.x1 - a.x2, 0.0)
y_gap = max(a.y1 - b.y2, b.y1 - a.y2, 0.0)
return x_gap, y_gap
def predicted_point(point, velocity, acceleration, t):
return (
point
+ velocity * t
+ 0.5 * acceleration * t * t
)
def point_to_segment_distance(p, a, b):
ab = b - a
denom = float(np.dot(ab, ab))
if denom < 1e-9:
return float(np.linalg.norm(p - a))
u = float(np.dot(p - a, ab) / denom)
u = clamp(u, 0.0, 1.0)
q = a + u * ab
return float(np.linalg.norm(p - q))
def trajectory_distance(
p1, v1, a1,
p2, v2, a2,
horizon=2.0,
step=0.05
):
"""
预测两个接地点未来轨迹的最小距离。
"""
best_d = float("inf")
best_t = float("inf")
t = 0.0
while t <= horizon + 1e-6:
q1 = predicted_point(p1, v1, a1, t)
q2 = predicted_point(p2, v2, a2, t)
d = float(np.linalg.norm(q1 - q2))
if d < best_d:
best_d = d
best_t = t
t += step
return best_d, best_t
def relative_ttc(p1, v1, p2, v2):
"""
点目标近似 TTC。
只在相对运动确实朝向彼此时返回有限值。
"""
dp = p2 - p1
dv = v2 - v1
vv = float(np.dot(dv, dv))
if vv < 1e-6:
return float("inf")
t = -float(np.dot(dp, dv)) / vv
if t < 0:
return float("inf")
closest = dp + dv * t
d = float(np.linalg.norm(closest))
return t, d
# ============================================================
# 风险计算
# ============================================================
class CollisionEngine:
"""
图像空间碰撞风险引擎。
"""
def __init__(
self,
fps=25.0,
warning_ttc=1.5,
critical_ttc=0.8,
collision_iou=0.05,
max_horizon=2.0,
min_history=5,
):
self.fps = fps
self.warning_ttc = warning_ttc
self.critical_ttc = critical_ttc
self.collision_iou = collision_iou
self.max_horizon = max_horizon
self.min_history = min_history
def _scale(self, a: Detection, b: Detection):
"""
自适应尺度。
不使用固定像素阈值。
"""
return max(
8.0,
0.5 * (
min(a.w, a.h) +
min(b.w, b.h)
)
)
def analyze(
self,
a: Detection,
b: Detection,
ta: TrackState,
tb: TrackState,
pair: PairState
):
if len(ta.points) < self.min_history or \
len(tb.points) < self.min_history:
return self._result(
"NORMAL", 0.0, float("inf"),
float("inf"), False, False
)
# ---------- 深度一致性过滤 ----------
# 接地点纵坐标差异过大,说明两车可能不在同一平面(远/近不同),
# 直接跳过,避免投影重叠引起的误检。
y_diff = abs(a.bottom_center[1] - b.bottom_center[1])
max_h = max(a.h, b.h)
if y_diff > 1.2 * max_h:
return self._result(
"NORMAL", 0.0, float("inf"),
float("inf"), False, False
)
va = ta.velocity
vb = tb.velocity
aa = ta.acceleration
ab = tb.acceleration
pa = a.bottom_center
pb = b.bottom_center
# 几何量计算
gap = bbox_gap(a.box, b.box)
iou = bbox_iou(a.box, b.box)
scale = self._scale(a, b)
normalized_gap = gap / scale
dp = pb - pa
dv = vb - va
distance = float(np.linalg.norm(dp))
approaching_speed = 0.0
if distance > 1e-6:
unit = dp / distance
approaching_speed = max(0.0, -float(np.dot(dv, unit)))
approaching = approaching_speed > max(1.0, 0.01 * scale * self.fps)
min_pred_dist, pred_t = trajectory_distance(
pa, va, aa,
pb, vb, ab,
horizon=self.max_horizon,
step=1.0 / max(10.0, self.fps)
)
normalized_pred_dist = min_pred_dist / scale
ttc_value = float("inf")
ttc_result = relative_ttc(pa, va, pb, vb)
if isinstance(ttc_result, tuple):
ttc, closest_d = ttc_result
if ttc <= self.max_horizon and closest_d <= scale * 1.5:
ttc_value = ttc
# 风险评分仅用于显示,不影响状态判定
proximity_score = sigmoid((1.0 - normalized_gap) * 4.0)
prediction_score = sigmoid((1.0 - normalized_pred_dist) * 4.0)
if math.isfinite(ttc_value):
ttc_score = clamp(1.0 - ttc_value / self.warning_ttc, 0.0, 1.0)
else:
ttc_score = 0.0
speed_score = sigmoid((approaching_speed / max(scale * 0.05, 1.0)) - 2.0)
contact_score = 1.0 if iou >= self.collision_iou else 0.0
risk = (0.15 * proximity_score + 0.30 * prediction_score +
0.20 * ttc_score + 0.15 * speed_score + 0.20 * contact_score)
# 不再设置 state,由状态机决定
return {
"state": "POTENTIAL", # 占位符,状态机忽略
"risk": float(risk),
"ttc": float(ttc_value),
"gap": float(gap),
"approaching": bool(approaching),
"contact": bool(iou >= self.collision_iou),
"pred_t": float(pred_t),
"pred_dist": float(min_pred_dist),
"iou": float(iou)
}
def collision_motion_features(self, ta: TrackState, tb: TrackState):
"""
提取两个目标的运动特征,用于碰撞验证。
"""
features = {
'a_angular': ta.angular_speed,
'b_angular': tb.angular_speed,
'a_decel': ta.longitudinal_decel,
'b_decel': tb.longitudinal_decel,
'max_angular': max(ta.angular_speed, tb.angular_speed),
'max_decel': max(ta.longitudinal_decel, tb.longitudinal_decel),
}
return features
@staticmethod
def _result(
state,
risk,
ttc,
gap,
approaching,
contact,
pred_t=float("inf"),
pred_dist=float("inf"),
iou=0.0
):
return {
"state": state,
"risk": float(risk),
"ttc": float(ttc),
"gap": float(gap),
"approaching": bool(approaching),
"contact": bool(contact),
"pred_t": float(pred_t),
"pred_dist": float(pred_dist),
"iou": float(iou)
}
# ============================================================
# 状态机(含运动学验证)
# ============================================================
class PairStateMachine:
# 碰撞运动阈值(需根据实际场景标定)
ANGULAR_THRESH = math.radians(15) # 15 度/秒
DECEL_THRESH = 150.0 # 像素/秒²,相当于急刹车
def __init__(
self,
warning_frames=3,
critical_frames=3,
collision_frames=3,
release_frames=8,
cooldown_frames=15,
use_motion_validation=True,
angular_threshold=math.radians(15),
decel_threshold=150.0,
contact_iou=0.05,
near_gap_pixels=30.0,
motion_strict=False,
ignore_geometry=False
):
self.warning_frames = warning_frames
self.critical_frames = critical_frames
self.collision_frames = collision_frames
self.release_frames = release_frames
self.cooldown_frames = cooldown_frames
self.use_motion_validation = use_motion_validation
self.angular_threshold = angular_threshold
self.decel_threshold = decel_threshold
self.contact_iou = contact_iou
self.near_gap_pixels = near_gap_pixels
self.motion_strict = motion_strict
self.ignore_geometry = ignore_geometry
def update(self, ps: PairState, result, motion_features=None):
# 冷却处理(保持不变)
if ps.state == "COLLISION":
if ps.cooldown > 0:
ps.cooldown -= 1
return ps.state
else:
ps.state = "NORMAL"
ps.warning_count = 0
ps.critical_count = 0
ps.collision_count = 0
ps.separating_count = 0
# 提取几何信息
iou = result.get("iou", 0.0)
gap = result.get("gap", float("inf"))
approaching = result.get("approaching", False)
# 几何接触判定
if self.ignore_geometry:
geom_contact = True # 直接忽略几何条件
else:
geom_contact = (iou >= self.contact_iou) or (gap <= self.near_gap_pixels)
# 运动学突变判定
motion_hit = False
if motion_features and self.use_motion_validation:
max_angular = motion_features["max_angular"]
max_decel = motion_features["max_decel"]
motion_hit = (max_angular >= self.angular_threshold and
max_decel >= self.decel_threshold)
# 只有几何接近且运动学突变同时满足,才视为碰撞候选
if geom_contact and motion_hit:
event = "COLLISION_CANDIDATE"
elif geom_contact and approaching:
event = "WARNING_CANDIDATE"
else:
event = "NORMAL"
# 更新计数器
if event == "COLLISION_CANDIDATE":
ps.collision_count += 1
ps.critical_count += 1
ps.warning_count += 1
ps.separating_count = 0
elif event == "WARNING_CANDIDATE":
ps.warning_count += 1
ps.critical_count = max(0, ps.critical_count - 1)
ps.collision_count = max(0, ps.collision_count - 1)
ps.separating_count = 0
else:
ps.warning_count = max(0, ps.warning_count - 1)
ps.critical_count = max(0, ps.critical_count - 1)
if not approaching:
ps.separating_count += 1
else:
ps.separating_count = 0
# 状态转移
if ps.collision_count >= self.collision_frames:
ps.state = "COLLISION"
ps.cooldown = self.cooldown_frames
ps.warning_count = 0
ps.critical_count = 0
ps.collision_count = 0
ps.separating_count = 0
return ps.state
if ps.critical_count >= self.critical_frames:
ps.state = "CRITICAL"
return ps.state
if ps.warning_count >= self.warning_frames:
ps.state = "WARNING"
return ps.state
if ps.separating_count >= self.release_frames:
ps.state = "NORMAL"
ps.critical_count = 0
ps.collision_count = 0
return ps.state
# ============================================================
# 绘制
# ============================================================
def state_color(state):
if state == "COLLISION":
return (0, 0, 255)
if state == "CRITICAL":
return (0, 80, 255)
if state == "WARNING":
return (0, 165, 255)
if state == "CONTACT":
return (0, 0, 255)
return (255, 255, 255)
def draw_track(frame, det: Detection, track: TrackState):
color = (255, 255, 255)
x1, y1, x2, y2 = map(int, det.box)
cv2.rectangle(
frame,
(x1, y1),
(x2, y2),
color,
2
)
label = (
f"{det.name} "
f"ID:{det.track_id} "
f"{det.conf:.2f}"
)
cv2.putText(
frame,
label,
(x1, max(20, y1 - 8)),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
color,
2,
cv2.LINE_AA
)
# 接地点
p = tuple(map(int, det.bottom_center))
cv2.circle(frame, p, 4, color, -1)
# 轨迹
pts = list(track.points)
for i in range(1, len(pts)):
p1 = tuple(map(int, pts[i - 1]))
p2 = tuple(map(int, pts[i]))
cv2.line(frame, p1, p2, color, 2, cv2.LINE_AA)
def draw_pair_info(frame, a: Detection, b: Detection, result, final_state):
"""
只在风险状态下显示简洁的多行信息,无连线。
显示两个相关目标的ID以及当前状态的原因参数。
"""
if final_state == "NORMAL":
return
c = state_color(final_state)
pa = tuple(map(int, a.bottom_center))
pb = tuple(map(int, b.bottom_center))
mid_x = int((pa[0] + pb[0]) * 0.5)
mid_y = int((pa[1] + pb[1]) * 0.5)
# 构建解释性文本
lines = [
f"ID {a.track_id} - ID {b.track_id} : {final_state}",
f"Risk {result['risk']:.2f} Gap {result['gap']:.1f} px",
f"TTC {result['ttc']:.2f}s PredDist {result['pred_dist']:.1f} px",
f"Approach {'Yes' if result['approaching'] else 'No'} IoU {result['iou']:.3f}",
]
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.45
thickness = 1
line_spacing = 18
for i, line in enumerate(lines):
text_y = mid_y - 30 - i * line_spacing
cv2.putText(frame, line, (mid_x - 120, text_y),
font, font_scale, c, thickness, cv2.LINE_AA)
# ============================================================
# YOLO结果解析
# ============================================================
def parse_detections(result):
detections = []
if result.boxes is None:
return detections
if result.boxes.id is None:
return detections
boxes = result.boxes.xyxy.cpu().numpy()
ids = result.boxes.id.cpu().numpy().astype(int)
classes = result.boxes.cls.cpu().numpy().astype(int)
confs = result.boxes.conf.cpu().numpy()
masks = None
if result.masks is not None:
try:
masks = result.masks.data.cpu().numpy()
except Exception:
masks = None
names = result.names
for i, (box, track_id, cls_id, conf) in enumerate(
zip(boxes, ids, classes, confs)
):
if isinstance(names, dict):
name = str(names.get(int(cls_id), cls_id))
else:
name = str(names[int(cls_id)])
mask = None
if masks is not None and i < len(masks):
mask = masks[i]
detections.append(
Detection(
track_id=int(track_id),
cls_id=int(cls_id),
name=name,
conf=float(conf),
box=np.asarray(box, dtype=np.float32),
mask=mask
)
)
return detections
# ============================================================
# 目标筛选
# ============================================================
def select_relevant(detections, mode="vehicle"):
if mode == "all":
return detections
result = []
for d in detections:
if d.is_vehicle:
result.append(d)
elif mode == "vehicle_obstacle" and d.is_obstacle:
result.append(d)
return result
# ============================================================
# 主检测器
# ============================================================
class CollisionDetector:
def __init__(
self,
model_path,
fps=25,
conf=0.25,
imgsz=1280,
tracker="bytetrack.yaml",
target_mode="vehicle_obstacle",
warning_frames=3,
critical_frames=3,
collision_frames=3,
release_frames=8,
use_motion_validation=True,
angular_threshold=math.radians(15),
decel_threshold=150.0,
contact_iou=0.05,
near_gap_pixels=30.0,
motion_strict=False,
show_detail=False, # 新增
ignore_geometry=False, # 新增
):
self.model = YOLO(model_path)
self.conf = conf
self.imgsz = imgsz
self.tracker = tracker
self.target_mode = target_mode
self.tracks: Dict[int, TrackState] = {}
self.pairs: Dict[Tuple[int, int], PairState] = defaultdict(PairState)
self.engine = CollisionEngine(fps=fps)
self.state_machine = PairStateMachine(
warning_frames=warning_frames,
critical_frames=critical_frames,
collision_frames=collision_frames,
release_frames=release_frames,
use_motion_validation=use_motion_validation,
angular_threshold=angular_threshold,
decel_threshold=decel_threshold,
contact_iou=contact_iou,
near_gap_pixels=near_gap_pixels,
motion_strict=motion_strict
)
self.frame_index = 0
self.show_detail = show_detail
def process(self, frame, timestamp):
self.frame_index += 1
# YOLO + ByteTrack
results = self.model.track(
frame,
persist=True,
tracker=self.tracker,
conf=self.conf,
imgsz=self.imgsz,
verbose=False
)
result = results[0]
detections = parse_detections(result)
detections = select_relevant(detections, self.target_mode)
current_ids = set()
# 更新轨迹
for d in detections:
current_ids.add(d.track_id)
if d.track_id not in self.tracks:
self.tracks[d.track_id] = TrackState(
track_id=d.track_id,
name=d.name
)
self.tracks[d.track_id].update(d, timestamp)
# 丢失轨迹处理
for tid, track in list(self.tracks.items()):
if tid not in current_ids:
track.missed += 1
if track.missed > 30:
del self.tracks[tid]
det_map = {d.track_id: d for d in detections}
# 绘制目标
for d in detections:
track = self.tracks.get(d.track_id)
if track is not None:
draw_track(frame, d, track)
# 两两碰撞分析
pair_results = []
active_pairs = [] # 新增:存储非正常状态的目标对
ids = list(det_map.keys())
for i in range(len(ids)):
for j in range(i + 1, len(ids)):
id1, id2 = ids[i], ids[j]
a = det_map[id1]
b = det_map[id2]
ta = self.tracks.get(id1)
tb = self.tracks.get(id2)
if ta is None or tb is None:
continue
if len(ta.points) < 5 or len(tb.points) < 5:
continue
key = tuple(sorted((id1, id2)))
result_pair = self.engine.analyze(a, b, ta, tb, self.pairs[key])
# 提取运动特征用于碰撞验证
motion_feat = self.engine.collision_motion_features(ta, tb)
ps = self.pairs[key]
ps.last_risk = result_pair["risk"]
ps.last_ttc = result_pair["ttc"]
ps.last_gap = result_pair["gap"]
final_state = self.state_machine.update(
ps, result_pair, motion_feat
)
pair_results.append((a, b, result_pair, final_state))
# 只收集非正常状态的对
if final_state != "NORMAL":
if self.show_detail:
detail = {
"iou": result_pair["iou"],
"gap": result_pair["gap"],
"approaching": result_pair["approaching"],
"max_angular": motion_feat["max_angular"],
"max_decel": motion_feat["max_decel"],
"geom_contact": (result_pair["iou"] >= self.state_machine.contact_iou) or
(result_pair["gap"] <= self.state_machine.near_gap_pixels),
"motion_hit": (motion_feat["max_angular"] >= self.state_machine.angular_threshold and
motion_feat["max_decel"] >= self.state_machine.decel_threshold),
"contact_iou_thresh": self.state_machine.contact_iou,
"near_gap_thresh": self.state_machine.near_gap_pixels,
"angular_thresh": self.state_machine.angular_threshold,
"decel_thresh": self.state_machine.decel_threshold,
}
active_pairs.append((a.track_id, b.track_id, final_state, detail))
else:
active_pairs.append((a.track_id, b.track_id, final_state))
# 全局状态
global_state = "NORMAL"
priority = {
"NORMAL": 0,
"WARNING": 1,
"CRITICAL": 2,
"COLLISION": 3
}
for _, _, _, state in pair_results:
if priority.get(state, 0) > priority[global_state]:
global_state = state
# ------------------ HUD 绘制 ------------------
# 原有状态栏
cv2.rectangle(frame, (10, 10), (360, 95), (0, 0, 0), -1)
cv2.putText(frame, f"STATE: {global_state}", (25, 45),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, state_color(global_state), 2, cv2.LINE_AA)
cv2.putText(frame, f"Objects: {len(detections)}", (25, 78),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2, cv2.LINE_AA)
# 新增:绘制活跃碰撞对信息(位于原有状态栏下方)
draw_active_pairs(frame, active_pairs, start_y=110)
return frame, global_state, pair_results
def draw_active_pairs(frame, active_pairs, start_y=110, show_detail=False):
"""
在左上角统一显示所有非正常状态的目标对。
active_pairs: list of tuples:
(id1, id2, state) 或 (id1, id2, state, detail_dict)
"""
if not active_pairs:
return
# 计算面板高度
line_height = 22
if show_detail:
# 每对最多占用 5 行(标题行 + 4 行详情)
panel_height = 30 + len(active_pairs) * line_height * 5
else:
panel_height = 30 + len(active_pairs) * line_height
panel_width = 480 # 加宽以容纳详情
# 半透明背景
overlay = frame.copy()
cv2.rectangle(overlay, (10, start_y), (10 + panel_width, start_y + panel_height), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.5, frame, 0.5, 0, frame)
cv2.putText(frame, "Active Pairs:", (25, start_y + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
y = start_y + 45
for item in active_pairs:
if len(item) == 3:
id1, id2, state = item
color = state_color(state)
text = f"ID {id1} - ID {id2} : {state}"
cv2.putText(frame, text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA)
y += line_height
else:
id1, id2, state, detail = item
color = state_color(state)
# 第一行:ID和状态
text = f"ID {id1} - ID {id2} : {state}"
cv2.putText(frame, text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA)
y += line_height
# 第二行:几何接触情况
geom_text = f"Geom: contact={detail['geom_contact']} (IoU={detail['iou']:.3f} >= {detail['contact_iou_thresh']:.2f}, Gap={detail['gap']:.1f} <= {detail['near_gap_thresh']:.1f})"
cv2.putText(frame, geom_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
# 第三行:运动突变情况
motion_text = f"Motion: hit={detail['motion_hit']} (Ang={math.degrees(detail['max_angular']):.1f}deg/s >= {math.degrees(detail['angular_thresh']):.1f}, Decel={detail['max_decel']:.1f}px/s^2 >= {detail['decel_thresh']:.1f})"
cv2.putText(frame, motion_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
# 第四行:是否接近
approach_text = f"Approaching: {'Yes' if detail['approaching'] else 'No'}"
cv2.putText(frame, approach_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
# 增加空行分隔
y += line_height // 2
# ============================================================
# 输入源
# ============================================================
def open_source(source):
if str(source).isdigit():
return cv2.VideoCapture(int(source))
return cv2.VideoCapture(source)
# ============================================================
# 主程序
# ============================================================
def main():
parser = argparse.ArgumentParser(description="Vehicle collision detection and warning")
parser.add_argument("--model", default=r"D:\zero_track\car_collision\checkpoints\yolo26s.pt",
help="YOLO .pt/.engine")
parser.add_argument("--source", default=r"D:\zero_track\car_collision\input\ACCIDENT Project Page_1015337455.mp4",
help="video path or camera index")
parser.add_argument("--output", default="collision_result.mp4")
parser.add_argument("--conf", type=float, default=0.25)
parser.add_argument("--imgsz", type=int, default=1280)
parser.add_argument("--target-mode", choices=["vehicle", "vehicle_obstacle", "all"],
default="vehicle_obstacle")
parser.add_argument("--tracker", default="bytetrack.yaml")
parser.add_argument("--no-save", action="store_true")
parser.add_argument("--show", action="store_true")
parser.add_argument("--no-motion-validation", action="store_true",
help="禁用运动学验证(默认启用)")
parser.add_argument("--angular-threshold", type=float, default=15.0,
help="角速度突变阈值(度/秒),默认15")
parser.add_argument("--decel-threshold", type=float, default=150.0,
help="减速度阈值(像素/秒^2),默认150")
parser.add_argument("--contact-iou", type=float, default=0.05,
help="判定几何接触的 IoU 阈值")
parser.add_argument("--near-gap", type=float, default=30.0,
help="判定几何接近的像素间隙阈值")
parser.add_argument("--motion-strict", action="store_true",
help="启用严格模式:仅当运动学突变时触发告警,忽略纯几何接近")
parser.add_argument("--show-detail", action="store_true", default=True,
help="在左上角显示非正常状态的详细判定原因")
parser.add_argument("--ignore-geometry", action="store_true",
help="忽略几何接触条件,仅凭运动学突变判定碰撞")
args = parser.parse_args()
cap = open_source(args.source)
if not cap.isOpened():
raise RuntimeError(f"Cannot open source: {args.source}")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 1:
fps = 25.0
print("=" * 70)
print("Collision Detector")
print("=" * 70)
print(f"Input : {width} x {height}")
print(f"FPS : {fps:.2f}")
print(f"Model : {args.model}")
print(f"Mode : {args.target_mode}")
print(f"Motion Validation: {'Enabled' if not args.no_motion_validation else 'Disabled'}")
print("=" * 70)
writer = None
if not args.no_save:
writer = cv2.VideoWriter(args.output, cv2.VideoWriter_fourcc(*"mp4v"),
fps, (width, height))
if not writer.isOpened():
raise RuntimeError(f"Cannot open output: {args.output}")
detector = CollisionDetector(
model_path=args.model,
fps=fps,
conf=args.conf,
imgsz=args.imgsz,
tracker=args.tracker,
target_mode=args.target_mode,
use_motion_validation=not args.no_motion_validation,
angular_threshold=math.radians(args.angular_threshold),
decel_threshold=args.decel_threshold,
contact_iou=args.contact_iou,
near_gap_pixels=args.near_gap,
motion_strict=args.motion_strict,
show_detail=args.show_detail
)
frame_index = 0
t0 = time.time()
last_print = time.time()
processed = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_index += 1
timestamp = frame_index / fps
output, state, pair_results = detector.process(frame, timestamp)
if writer is not None:
writer.write(output)
if args.show:
cv2.imshow("Collision Detector", output)
key = cv2.waitKey(1) & 0xFF
if key == 27:
break
processed += 1
if time.time() - last_print > 1.0:
elapsed = time.time() - t0
current_fps = processed / elapsed if elapsed > 0 else 0
print(f"\rFrame={frame_index:6d} FPS={current_fps:6.2f} State={state:10s}",
end="", flush=True)
last_print = time.time()
finally:
cap.release()
if writer is not None:
writer.release()
cv2.destroyAllWindows()
print()
print("=" * 70)
print("Finished")
print(f"Output: {args.output}")
print("=" * 70)
if __name__ == "__main__":
main()
三、车辆碰撞1.5——误检减少
python collision1.5.py ^
--contact-iou 0.15 ^
--contact-gap-normalized 0.10 ^
--vehicle-collision-ttc 0.3 ^
--obstacle-collision-ttc 0.4 ^
--decel-threshold 250 ^
--collision-frames 5 ^
--critical-frames 4 ^
--warning-frames 4 ^
--release-frames 10 ^
--motion-confirm-frames 3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
collision_detector.py
第二版:车辆/障碍物碰撞风险检测
--------------------------------------
核心改进:
1. 保留 YOLO + ByteTrack 跟踪
2. 使用 bbox 几何关系 + 运动学验证(角速度突变 + 急减速)区分
自然行驶与碰撞后的异常状态
3. 显示优化:移除连线,只在风险状态显示关键信息
4. 修复状态机冷却逻辑,增加类型安全处理
5. 针对跟车误检:引入 TTC 为核心判定,区分车辆-车辆与车辆-障碍物
6. 针对障碍物碰撞:障碍物目标速度置零,运动学验证对障碍物适当放宽
7. 补齐命令行参数,修正状态机计数逻辑,增加运动学确认帧
"""
import argparse
import math
import time
from dataclasses import dataclass, field
from collections import defaultdict, deque
from typing import Dict, List, Tuple, Optional
import cv2
import numpy as np
from ultralytics import YOLO
# ============================================================
# 类别配置
# ============================================================
DEFAULT_VEHICLE_NAMES = {
"car", "truck", "bus", "motorcycle", "motorbike", "van"
}
DEFAULT_OBSTACLE_NAMES = {
"barrier", "guardrail", "road_obstacle", "obstacle",
"cone", "traffic_cone", "roadblock", "bollard"
}
# ============================================================
# 数据结构
# ============================================================
@dataclass
class Detection:
track_id: int
cls_id: int
name: str
conf: float
box: np.ndarray # x1,y1,x2,y2
mask: Optional[np.ndarray] = None
@property
def x1(self):
return float(self.box[0])
@property
def y1(self):
return float(self.box[1])
@property
def x2(self):
return float(self.box[2])
@property
def y2(self):
return float(self.box[3])
@property
def w(self):
return max(1.0, self.x2 - self.x1)
@property
def h(self):
return max(1.0, self.y2 - self.y1)
@property
def center(self):
return np.array([(self.x1 + self.x2) * 0.5,
(self.y1 + self.y2) * 0.5], dtype=np.float32)
@property
def bottom_center(self):
return np.array([(self.x1 + self.x2) * 0.5,
self.y2], dtype=np.float32)
@property
def bottom_left(self):
return np.array([self.x1, self.y2], dtype=np.float32)
@property
def bottom_right(self):
return np.array([self.x2, self.y2], dtype=np.float32)
@property
def is_vehicle(self):
return str(self.name).lower() in DEFAULT_VEHICLE_NAMES
@property
def is_obstacle(self):
return str(self.name).lower() in DEFAULT_OBSTACLE_NAMES
@dataclass
class TrackState:
track_id: int
name: str
points: deque = field(default_factory=lambda: deque(maxlen=20))
boxes: deque = field(default_factory=lambda: deque(maxlen=20))
timestamps: deque = field(default_factory=lambda: deque(maxlen=20))
velocities: deque = field(default_factory=lambda: deque(maxlen=10))
missed: int = 0
def update(self, det: Detection, t: float):
self.points.append(det.bottom_center.copy())
self.boxes.append(det.box.copy())
self.timestamps.append(t)
self.missed = 0
if len(self.points) >= 4:
vel = self.velocity
self.velocities.append(vel.copy())
@property
def velocity(self):
if len(self.points) < 4:
return np.zeros(2, dtype=np.float32)
n = min(8, len(self.points))
p = np.asarray(list(self.points)[-n:], dtype=np.float32)
ts = np.asarray(list(self.timestamps)[-n:], dtype=np.float64)
dt = ts - ts[0]
if dt[-1] <= 1e-6:
return np.zeros(2, dtype=np.float32)
vx = np.polyfit(dt, p[:, 0], 1)[0]
vy = np.polyfit(dt, p[:, 1], 1)[0]
return np.array([vx, vy], dtype=np.float32)
@property
def acceleration(self):
if len(self.points) < 6:
return np.zeros(2, dtype=np.float32)
n = min(10, len(self.points))
p = np.asarray(list(self.points)[-n:], dtype=np.float32)
ts = np.asarray(list(self.timestamps)[-n:], dtype=np.float64)
v = []
for i in range(1, len(p)):
dt = ts[i] - ts[i - 1]
if dt > 1e-6:
v.append((p[i] - p[i - 1]) / dt)
if len(v) < 3:
return np.zeros(2, dtype=np.float32)
v = np.asarray(v)
dt = np.arange(len(v), dtype=np.float32)
ax = np.polyfit(dt, v[:, 0], 1)[0]
ay = np.polyfit(dt, v[:, 1], 1)[0]
return np.array([ax, ay], dtype=np.float32)
@property
def angular_speed(self):
if len(self.velocities) < 2:
return 0.0
v1 = self.velocities[-2]
v2 = self.velocities[-1]
if len(self.timestamps) < 2:
return 0.0
dt = self.timestamps[-1] - self.timestamps[-2]
if dt < 1e-3:
return 0.0
angle1 = math.atan2(v1[1], v1[0])
angle2 = math.atan2(v2[1], v2[0])
dangle = math.atan2(math.sin(angle2 - angle1), math.cos(angle2 - angle1))
return abs(dangle) / dt
@property
def longitudinal_decel(self):
if len(self.velocities) < 2:
return 0.0
v1 = self.velocities[-2]
v2 = self.velocities[-1]
if len(self.timestamps) < 2:
return 0.0
dt = self.timestamps[-1] - self.timestamps[-2]
if dt < 1e-3:
return 0.0
norm_v1 = float(np.linalg.norm(v1))
if norm_v1 < 1e-3:
return 0.0
unit_v1 = v1 / norm_v1
delta_v = v2 - v1
decel = -float(np.dot(delta_v, unit_v1)) / dt
return max(0.0, decel)
@property
def last_box(self):
return self.boxes[-1] if self.boxes else None
@property
def last_point(self):
return self.points[-1] if self.points else None
@dataclass
class PairState:
warning_count: int = 0
critical_count: int = 0
collision_count: int = 0
separating_count: int = 0
cooldown: int = 0
motion_hit_count: int = 0
last_risk: float = 0.0
last_ttc: float = float("inf")
last_gap: float = float("inf")
state: str = "NORMAL"
# ============================================================
# 几何工具
# ============================================================
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def sigmoid(x):
x = clamp(float(x), -30.0, 30.0)
return 1.0 / (1.0 + math.exp(-x))
def bbox_iou(a, b):
x1 = max(a[0], b[0])
y1 = max(a[1], b[1])
x2 = min(a[2], b[2])
y2 = min(a[3], b[3])
iw = max(0.0, x2 - x1)
ih = max(0.0, y2 - y1)
inter = iw * ih
aa = max(0.0, a[2] - a[0]) * max(0.0, a[3] - a[1])
ab = max(0.0, b[2] - b[0]) * max(0.0, b[3] - b[1])
union = aa + ab - inter
return inter / union if union > 1e-6 else 0.0
def bbox_gap(a, b):
dx = max(a[0] - b[2], b[0] - a[2], 0.0)
dy = max(a[1] - b[3], b[1] - a[3], 0.0)
return math.hypot(dx, dy)
def predicted_point(point, velocity, acceleration, t):
return (
point
+ velocity * t
+ 0.5 * acceleration * t * t
)
def trajectory_distance(
p1, v1, a1,
p2, v2, a2,
horizon=2.0,
step=0.05
):
best_d = float("inf")
best_t = float("inf")
t = 0.0
while t <= horizon + 1e-6:
q1 = predicted_point(p1, v1, a1, t)
q2 = predicted_point(p2, v2, a2, t)
d = float(np.linalg.norm(q1 - q2))
if d < best_d:
best_d = d
best_t = t
t += step
return best_d, best_t
def relative_ttc(p1, v1, p2, v2):
dp = p2 - p1
dv = v2 - v1
vv = float(np.dot(dv, dv))
if vv < 1e-6:
return float("inf")
t = -float(np.dot(dp, dv)) / vv
if t < 0:
return float("inf")
closest = dp + dv * t
d = float(np.linalg.norm(closest))
return t, d
# ============================================================
# 风险计算
# ============================================================
class CollisionEngine:
def __init__(
self,
fps=25.0,
warning_ttc=1.5,
critical_ttc=0.8,
collision_iou=0.05,
max_horizon=2.0,
min_history=5,
speed_eps=2.0,
):
self.fps = fps
self.warning_ttc = warning_ttc
self.critical_ttc = critical_ttc
self.collision_iou = collision_iou
self.max_horizon = max_horizon
self.min_history = min_history
self.speed_eps = speed_eps
def _scale(self, a: Detection, b: Detection):
return max(
8.0,
0.5 * (
min(a.w, a.h) +
min(b.w, b.h)
)
)
def analyze(
self,
a: Detection,
b: Detection,
ta: TrackState,
tb: TrackState,
pair: PairState
):
if len(ta.points) < self.min_history or \
len(tb.points) < self.min_history:
return self._result(
"NORMAL", 0.0, float("inf"),
float("inf"), False, False
)
y_diff = abs(a.bottom_center[1] - b.bottom_center[1])
max_h = max(a.h, b.h)
if y_diff > 1.2 * max_h:
return self._result(
"NORMAL", 0.0, float("inf"),
float("inf"), False, False
)
va = ta.velocity
vb = tb.velocity
aa = ta.acceleration
ab = tb.acceleration
if a.is_obstacle:
va = np.zeros(2, dtype=np.float32)
aa = np.zeros(2, dtype=np.float32)
if b.is_obstacle:
vb = np.zeros(2, dtype=np.float32)
ab = np.zeros(2, dtype=np.float32)
pa = a.bottom_center
pb = b.bottom_center
gap = bbox_gap(a.box, b.box)
iou = bbox_iou(a.box, b.box)
scale = self._scale(a, b)
normalized_gap = gap / scale
dp = pb - pa
dv = vb - va
distance = float(np.linalg.norm(dp))
approaching_speed = 0.0
if distance > 1e-6:
unit = dp / distance
approaching_speed = max(0.0, -float(np.dot(dv, unit)))
approaching = approaching_speed > max(1.0, 0.01 * scale * self.fps)
min_pred_dist, pred_t = trajectory_distance(
pa, va, aa,
pb, vb, ab,
horizon=self.max_horizon,
step=1.0 / max(10.0, self.fps)
)
normalized_pred_dist = min_pred_dist / scale
ttc_value = float("inf")
# 相对速度过小时不计算 TTC,避免噪声
if float(np.linalg.norm(dv)) > self.speed_eps:
ttc_result = relative_ttc(pa, va, pb, vb)
if isinstance(ttc_result, tuple):
ttc, closest_d = ttc_result
if ttc <= self.max_horizon and closest_d <= scale * 1.5:
ttc_value = ttc
proximity_score = sigmoid((1.0 - normalized_gap) * 4.0)
prediction_score = sigmoid((1.0 - normalized_pred_dist) * 4.0)
if math.isfinite(ttc_value):
ttc_score = clamp(1.0 - ttc_value / self.warning_ttc, 0.0, 1.0)
else:
ttc_score = 0.0
speed_score = sigmoid((approaching_speed / max(scale * 0.05, 1.0)) - 2.0)
contact_score = 1.0 if iou >= self.collision_iou else 0.0
risk = (0.15 * proximity_score + 0.30 * prediction_score +
0.20 * ttc_score + 0.15 * speed_score + 0.20 * contact_score)
return {
"risk": float(risk),
"ttc": float(ttc_value),
"gap": float(gap),
"approaching": bool(approaching),
"contact": bool(iou >= self.collision_iou),
"pred_t": float(pred_t),
"pred_dist": float(min_pred_dist),
"iou": float(iou),
"scale": float(scale),
"normalized_gap": float(normalized_gap),
"normalized_pred_dist": float(normalized_pred_dist)
}
def collision_motion_features(self, ta: TrackState, tb: TrackState):
features = {
'a_angular': ta.angular_speed,
'b_angular': tb.angular_speed,
'a_decel': ta.longitudinal_decel,
'b_decel': tb.longitudinal_decel,
'max_angular': max(ta.angular_speed, tb.angular_speed),
'max_decel': max(ta.longitudinal_decel, tb.longitudinal_decel),
}
return features
@staticmethod
def _result(
state,
risk,
ttc,
gap,
approaching,
contact,
pred_t=float("inf"),
pred_dist=float("inf"),
iou=0.0,
scale=0.0,
normalized_gap=float("inf"),
normalized_pred_dist=float("inf")
):
return {
"risk": float(risk),
"ttc": float(ttc),
"gap": float(gap),
"approaching": bool(approaching),
"contact": bool(contact),
"pred_t": float(pred_t),
"pred_dist": float(pred_dist),
"iou": float(iou),
"scale": float(scale),
"normalized_gap": float(normalized_gap),
"normalized_pred_dist": float(normalized_pred_dist)
}
# ============================================================
# 状态机(修正版)
# ============================================================
class PairStateMachine:
def __init__(
self,
warning_frames=3,
critical_frames=3,
collision_frames=3,
release_frames=8,
cooldown_frames=15,
use_motion_validation=True,
angular_threshold=math.radians(15),
decel_threshold=150.0,
contact_iou=0.05,
near_gap_pixels=30.0,
motion_strict=False,
ignore_geometry=False,
vehicle_warning_ttc=1.5,
vehicle_critical_ttc=0.8,
vehicle_collision_ttc=0.5,
obstacle_warning_ttc=2.5,
obstacle_critical_ttc=1.2,
obstacle_collision_ttc=0.8,
near_gap_normalized=0.8,
contact_gap_normalized=0.15,
motion_confirm_frames=2,
):
self.warning_frames = warning_frames
self.critical_frames = critical_frames
self.collision_frames = collision_frames
self.release_frames = release_frames
self.cooldown_frames = cooldown_frames
self.use_motion_validation = use_motion_validation
self.angular_threshold = angular_threshold
self.decel_threshold = decel_threshold
self.contact_iou = contact_iou
self.near_gap_pixels = near_gap_pixels
self.motion_strict = motion_strict
self.ignore_geometry = ignore_geometry
self.vehicle_warning_ttc = vehicle_warning_ttc
self.vehicle_critical_ttc = vehicle_critical_ttc
self.vehicle_collision_ttc = vehicle_collision_ttc
self.obstacle_warning_ttc = obstacle_warning_ttc
self.obstacle_critical_ttc = obstacle_critical_ttc
self.obstacle_collision_ttc = obstacle_collision_ttc
self.near_gap_normalized = near_gap_normalized
self.contact_gap_normalized = contact_gap_normalized
self.motion_confirm_frames = motion_confirm_frames
def update(self, ps: PairState, result, motion_features=None, pair_type="vehicle_vehicle"):
if ps.state == "COLLISION":
if ps.cooldown > 0:
ps.cooldown -= 1
return ps.state
else:
ps.state = "NORMAL"
ps.warning_count = 0
ps.critical_count = 0
ps.collision_count = 0
ps.motion_hit_count = 0
ps.separating_count = 0
iou = result.get("iou", 0.0)
normalized_gap = result.get("normalized_gap", float("inf"))
ttc = result.get("ttc", float("inf"))
approaching = result.get("approaching", False)
if pair_type == "vehicle_obstacle":
warn_ttc = self.obstacle_warning_ttc
crit_ttc = self.obstacle_critical_ttc
coll_ttc = self.obstacle_collision_ttc
else:
warn_ttc = self.vehicle_warning_ttc
crit_ttc = self.vehicle_critical_ttc
coll_ttc = self.vehicle_collision_ttc
if self.ignore_geometry:
geom_close = True
geom_contact = True
else:
geom_close = (iou >= self.contact_iou) or (normalized_gap < self.near_gap_normalized)
geom_contact = (iou >= self.contact_iou) or (normalized_gap < self.contact_gap_normalized)
motion_hit = False
if motion_features and self.use_motion_validation:
max_angular = motion_features.get("max_angular", 0.0)
max_decel = motion_features.get("max_decel", 0.0)
if pair_type == "vehicle_obstacle":
motion_hit = (max_decel >= self.decel_threshold) or \
(max_angular >= self.angular_threshold * 1.5)
else:
motion_hit = (max_angular >= self.angular_threshold and
max_decel >= self.decel_threshold)
if motion_hit:
ps.motion_hit_count += 1
else:
ps.motion_hit_count = 0
motion_confirmed = ps.motion_hit_count >= self.motion_confirm_frames
if geom_contact and (ttc < coll_ttc or motion_confirmed):
event = "COLLISION_CANDIDATE"
elif geom_close and approaching and ttc < crit_ttc:
event = "CRITICAL_CANDIDATE"
elif geom_close and approaching and ttc < warn_ttc:
event = "WARNING_CANDIDATE"
else:
event = "NORMAL"
if event == "COLLISION_CANDIDATE":
ps.collision_count += 1
ps.critical_count = 0
ps.warning_count = 0
ps.separating_count = 0
elif event == "CRITICAL_CANDIDATE":
ps.critical_count += 1
ps.collision_count = max(0, ps.collision_count - 1)
ps.warning_count = 0
ps.separating_count = 0
elif event == "WARNING_CANDIDATE":
ps.warning_count += 1
ps.critical_count = max(0, ps.critical_count - 1)
ps.collision_count = max(0, ps.collision_count - 1)
ps.separating_count = 0
else:
ps.warning_count = max(0, ps.warning_count - 1)
ps.critical_count = max(0, ps.critical_count - 1)
ps.collision_count = max(0, ps.collision_count - 1)
if not approaching:
ps.separating_count += 1
else:
ps.separating_count = 0
if ps.collision_count >= self.collision_frames:
ps.state = "COLLISION"
ps.cooldown = self.cooldown_frames
ps.warning_count = 0
ps.critical_count = 0
ps.collision_count = 0
ps.motion_hit_count = 0
ps.separating_count = 0
return ps.state
if ps.critical_count >= self.critical_frames:
ps.state = "CRITICAL"
return ps.state
if ps.warning_count >= self.warning_frames:
ps.state = "WARNING"
return ps.state
if ps.separating_count >= self.release_frames:
ps.state = "NORMAL"
ps.critical_count = 0
ps.collision_count = 0
ps.warning_count = 0
return ps.state
# ============================================================
# 绘制
# ============================================================
def state_color(state):
if state == "COLLISION":
return (0, 0, 255)
if state == "CRITICAL":
return (0, 80, 255)
if state == "WARNING":
return (0, 165, 255)
if state == "CONTACT":
return (0, 0, 255)
return (255, 255, 255)
def draw_track(frame, det: Detection, track: TrackState):
color = (255, 255, 255)
x1, y1, x2, y2 = map(int, det.box)
cv2.rectangle(
frame,
(x1, y1),
(x2, y2),
color,
2
)
label = (
f"{det.name} "
f"ID:{det.track_id} "
f"{det.conf:.2f}"
)
cv2.putText(
frame,
label,
(x1, max(20, y1 - 8)),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
color,
2,
cv2.LINE_AA
)
p = tuple(map(int, det.bottom_center))
cv2.circle(frame, p, 4, color, -1)
pts = list(track.points)
for i in range(1, len(pts)):
p1 = tuple(map(int, pts[i - 1]))
p2 = tuple(map(int, pts[i]))
cv2.line(frame, p1, p2, color, 2, cv2.LINE_AA)
def draw_pair_info(frame, a: Detection, b: Detection, result, final_state):
if final_state == "NORMAL":
return
c = state_color(final_state)
pa = tuple(map(int, a.bottom_center))
pb = tuple(map(int, b.bottom_center))
mid_x = int((pa[0] + pb[0]) * 0.5)
mid_y = int((pa[1] + pb[1]) * 0.5)
lines = [
f"ID {a.track_id} - ID {b.track_id} : {final_state}",
f"Risk {result['risk']:.2f} Gap {result['gap']:.1f} px",
f"TTC {result['ttc']:.2f}s PredDist {result['pred_dist']:.1f} px",
f"Approach {'Yes' if result['approaching'] else 'No'} IoU {result['iou']:.3f}",
]
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.45
thickness = 1
line_spacing = 18
for i, line in enumerate(lines):
text_y = mid_y - 30 - i * line_spacing
cv2.putText(frame, line, (mid_x - 120, text_y),
font, font_scale, c, thickness, cv2.LINE_AA)
# ============================================================
# YOLO结果解析
# ============================================================
def parse_detections(result):
detections = []
if result.boxes is None:
return detections
if result.boxes.id is None:
return detections
boxes = result.boxes.xyxy.cpu().numpy()
ids = result.boxes.id.cpu().numpy().astype(int)
classes = result.boxes.cls.cpu().numpy().astype(int)
confs = result.boxes.conf.cpu().numpy()
masks = None
if result.masks is not None:
try:
masks = result.masks.data.cpu().numpy()
except Exception:
masks = None
names = result.names
for i, (box, track_id, cls_id, conf) in enumerate(
zip(boxes, ids, classes, confs)
):
if isinstance(names, dict):
name = str(names.get(int(cls_id), cls_id))
else:
name = str(names[int(cls_id)])
mask = None
if masks is not None and i < len(masks):
mask = masks[i]
detections.append(
Detection(
track_id=int(track_id),
cls_id=int(cls_id),
name=name,
conf=float(conf),
box=np.asarray(box, dtype=np.float32),
mask=mask
)
)
return detections
# ============================================================
# 目标筛选
# ============================================================
def select_relevant(detections, mode="vehicle"):
if mode == "all":
return detections
result = []
for d in detections:
if d.is_vehicle:
result.append(d)
elif mode == "vehicle_obstacle" and d.is_obstacle:
result.append(d)
return result
# ============================================================
# 主检测器
# ============================================================
class CollisionDetector:
def __init__(
self,
model_path,
fps=25,
conf=0.25,
imgsz=1280,
tracker="bytetrack.yaml",
target_mode="vehicle_obstacle",
warning_frames=3,
critical_frames=3,
collision_frames=3,
release_frames=8,
cooldown_frames=15,
use_motion_validation=True,
angular_threshold=math.radians(15),
decel_threshold=150.0,
contact_iou=0.05,
near_gap_pixels=30.0,
motion_strict=False,
show_detail=False,
ignore_geometry=False,
vehicle_warning_ttc=1.5,
vehicle_critical_ttc=0.8,
vehicle_collision_ttc=0.5,
obstacle_warning_ttc=2.5,
obstacle_critical_ttc=1.2,
obstacle_collision_ttc=0.8,
near_gap_normalized=0.8,
contact_gap_normalized=0.15,
motion_confirm_frames=2,
speed_eps=2.0,
):
self.model = YOLO(model_path)
self.conf = conf
self.imgsz = imgsz
self.tracker = tracker
self.target_mode = target_mode
self.tracks: Dict[int, TrackState] = {}
self.pairs: Dict[Tuple[int, int], PairState] = defaultdict(PairState)
self.engine = CollisionEngine(
fps=fps,
warning_ttc=vehicle_warning_ttc,
critical_ttc=vehicle_critical_ttc,
collision_iou=contact_iou,
max_horizon=2.0,
min_history=5,
speed_eps=speed_eps,
)
self.state_machine = PairStateMachine(
warning_frames=warning_frames,
critical_frames=critical_frames,
collision_frames=collision_frames,
release_frames=release_frames,
cooldown_frames=cooldown_frames,
use_motion_validation=use_motion_validation,
angular_threshold=angular_threshold,
decel_threshold=decel_threshold,
contact_iou=contact_iou,
near_gap_pixels=near_gap_pixels,
motion_strict=motion_strict,
ignore_geometry=ignore_geometry,
vehicle_warning_ttc=vehicle_warning_ttc,
vehicle_critical_ttc=vehicle_critical_ttc,
vehicle_collision_ttc=vehicle_collision_ttc,
obstacle_warning_ttc=obstacle_warning_ttc,
obstacle_critical_ttc=obstacle_critical_ttc,
obstacle_collision_ttc=obstacle_collision_ttc,
near_gap_normalized=near_gap_normalized,
contact_gap_normalized=contact_gap_normalized,
motion_confirm_frames=motion_confirm_frames,
)
self.frame_index = 0
self.show_detail = show_detail
def process(self, frame, timestamp):
self.frame_index += 1
results = self.model.track(
frame,
persist=True,
tracker=self.tracker,
conf=self.conf,
imgsz=self.imgsz,
verbose=False
)
result = results[0]
detections = parse_detections(result)
detections = select_relevant(detections, self.target_mode)
current_ids = set()
for d in detections:
current_ids.add(d.track_id)
if d.track_id not in self.tracks:
self.tracks[d.track_id] = TrackState(
track_id=d.track_id,
name=d.name
)
self.tracks[d.track_id].update(d, timestamp)
for tid, track in list(self.tracks.items()):
if tid not in current_ids:
track.missed += 1
if track.missed > 30:
del self.tracks[tid]
det_map = {d.track_id: d for d in detections}
for d in detections:
track = self.tracks.get(d.track_id)
if track is not None:
draw_track(frame, d, track)
pair_results = []
active_pairs = []
ids = list(det_map.keys())
for i in range(len(ids)):
for j in range(i + 1, len(ids)):
id1, id2 = ids[i], ids[j]
a = det_map[id1]
b = det_map[id2]
ta = self.tracks.get(id1)
tb = self.tracks.get(id2)
if ta is None or tb is None:
continue
if len(ta.points) < 5 or len(tb.points) < 5:
continue
key = tuple(sorted((id1, id2)))
result_pair = self.engine.analyze(a, b, ta, tb, self.pairs[key])
motion_feat = self.engine.collision_motion_features(ta, tb)
if a.is_vehicle and b.is_vehicle:
pair_type = "vehicle_vehicle"
elif (a.is_vehicle and b.is_obstacle) or (a.is_obstacle and b.is_vehicle):
pair_type = "vehicle_obstacle"
else:
pair_type = "other"
ps = self.pairs[key]
ps.last_risk = result_pair["risk"]
ps.last_ttc = result_pair["ttc"]
ps.last_gap = result_pair["gap"]
final_state = self.state_machine.update(
ps, result_pair, motion_feat,
pair_type=pair_type
)
pair_results.append((a, b, result_pair, final_state))
if final_state != "NORMAL":
if self.show_detail:
max_angular = motion_feat.get("max_angular", 0.0)
max_decel = motion_feat.get("max_decel", 0.0)
if pair_type == "vehicle_obstacle":
motion_hit = (max_decel >= self.state_machine.decel_threshold) or \
(max_angular >= self.state_machine.angular_threshold * 1.5)
else:
motion_hit = (max_angular >= self.state_machine.angular_threshold and
max_decel >= self.state_machine.decel_threshold)
detail = {
"iou": result_pair["iou"],
"gap": result_pair["gap"],
"approaching": result_pair["approaching"],
"max_angular": motion_feat["max_angular"],
"max_decel": motion_feat["max_decel"],
"geom_contact": (result_pair["iou"] >= self.state_machine.contact_iou) or
(result_pair["normalized_gap"] < self.state_machine.contact_gap_normalized),
"contact_iou_thresh": self.state_machine.contact_iou,
"near_gap_thresh": self.state_machine.near_gap_pixels,
"angular_thresh": self.state_machine.angular_threshold,
"decel_thresh": self.state_machine.decel_threshold,
"normalized_gap": result_pair["normalized_gap"],
"motion_hit": motion_hit,
}
active_pairs.append((a.track_id, b.track_id, final_state, detail))
else:
active_pairs.append((a.track_id, b.track_id, final_state))
global_state = "NORMAL"
priority = {
"NORMAL": 0,
"WARNING": 1,
"CRITICAL": 2,
"COLLISION": 3
}
for _, _, _, state in pair_results:
if priority.get(state, 0) > priority[global_state]:
global_state = state
cv2.rectangle(frame, (10, 10), (360, 95), (0, 0, 0), -1)
cv2.putText(frame, f"STATE: {global_state}", (25, 45),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, state_color(global_state), 2, cv2.LINE_AA)
cv2.putText(frame, f"Objects: {len(detections)}", (25, 78),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2, cv2.LINE_AA)
draw_active_pairs(frame, active_pairs, start_y=110, show_detail=self.show_detail)
return frame, global_state, pair_results
def draw_active_pairs(frame, active_pairs, start_y=110, show_detail=False):
if not active_pairs:
return
line_height = 22
if show_detail:
panel_height = 30 + len(active_pairs) * line_height * 5
else:
panel_height = 30 + len(active_pairs) * line_height
panel_width = 480
overlay = frame.copy()
cv2.rectangle(overlay, (10, start_y), (10 + panel_width, start_y + panel_height), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.5, frame, 0.5, 0, frame)
cv2.putText(frame, "Active Pairs:", (25, start_y + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
y = start_y + 45
for item in active_pairs:
if len(item) == 3:
id1, id2, state = item
color = state_color(state)
text = f"ID {id1} - ID {id2} : {state}"
cv2.putText(frame, text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA)
y += line_height
else:
id1, id2, state, detail = item
color = state_color(state)
text = f"ID {id1} - ID {id2} : {state}"
cv2.putText(frame, text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA)
y += line_height
geom_text = f"Geom: contact={detail['geom_contact']} (IoU={detail['iou']:.3f} >= {detail['contact_iou_thresh']:.2f}, normGap={detail['normalized_gap']:.2f})"
cv2.putText(frame, geom_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
motion_text = f"Motion: hit={detail['motion_hit']} (Ang={math.degrees(detail['max_angular']):.1f}deg/s >= {math.degrees(detail['angular_thresh']):.1f}, Decel={detail['max_decel']:.1f}px/s^2 >= {detail['decel_thresh']:.1f})"
cv2.putText(frame, motion_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
approach_text = f"Approaching: {'Yes' if detail['approaching'] else 'No'}"
cv2.putText(frame, approach_text, (25, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA)
y += line_height
y += line_height // 2
# ============================================================
# 输入源
# ============================================================
def open_source(source):
if str(source).isdigit():
return cv2.VideoCapture(int(source))
return cv2.VideoCapture(source)
# ============================================================
# 主程序
# ============================================================
def main():
parser = argparse.ArgumentParser(description="Vehicle collision detection and warning")
parser.add_argument("--model", default=r"D:\zero_track\car_collision\checkpoints\yolo26s.pt",
help="YOLO .pt/.engine")
parser.add_argument("--source", default=r"D:\zero_track\car_collision\input\ACCIDENT Project Page_1015337455.mp4",
help="video path or camera index")
parser.add_argument("--output", default="collision_result.mp4")
parser.add_argument("--conf", type=float, default=0.25)
parser.add_argument("--imgsz", type=int, default=1280)
parser.add_argument("--target-mode", choices=["vehicle", "vehicle_obstacle", "all"],
default="vehicle_obstacle")
parser.add_argument("--tracker", default="bytetrack.yaml")
parser.add_argument("--no-save", action="store_true")
parser.add_argument("--show", action="store_true")
parser.add_argument("--no-motion-validation", action="store_true",
help="禁用运动学验证(默认启用)")
parser.add_argument("--angular-threshold", type=float, default=15.0,
help="角速度突变阈值(度/秒),默认15")
parser.add_argument("--decel-threshold", type=float, default=150.0,
help="减速度阈值(像素/秒^2),默认150")
parser.add_argument("--contact-iou", type=float, default=0.05,
help="判定几何接触的 IoU 阈值")
parser.add_argument("--near-gap", type=float, default=30.0,
help="判定几何接近的像素间隙阈值(已弃用,保留兼容)")
parser.add_argument("--motion-strict", action="store_true",
help="启用严格模式:仅当运动学突变时触发告警,忽略纯几何接近")
parser.add_argument("--show-detail", action="store_true", default=True,
help="在左上角显示非正常状态的详细判定原因")
parser.add_argument("--ignore-geometry", action="store_true",
help="忽略几何接触条件,仅凭运动学突变判定碰撞")
# 新增帧数参数
parser.add_argument("--warning-frames", type=int, default=3,
help="警告状态确认所需连续帧数")
parser.add_argument("--critical-frames", type=int, default=3,
help="临界状态确认所需连续帧数")
parser.add_argument("--collision-frames", type=int, default=3,
help="碰撞状态确认所需连续帧数")
parser.add_argument("--release-frames", type=int, default=8,
help="状态释放所需分离帧数")
parser.add_argument("--cooldown-frames", type=int, default=15,
help="碰撞后冷却帧数")
parser.add_argument("--motion-confirm-frames", type=int, default=2,
help="运动学突变确认所需连续帧数")
# TTC 阈值参数
parser.add_argument("--vehicle-warning-ttc", type=float, default=1.5,
help="车辆-车辆警告 TTC 阈值(秒)")
parser.add_argument("--vehicle-critical-ttc", type=float, default=0.8,
help="车辆-车辆临界 TTC 阈值(秒)")
parser.add_argument("--vehicle-collision-ttc", type=float, default=0.5,
help="车辆-车辆碰撞 TTC 阈值(秒)")
parser.add_argument("--obstacle-warning-ttc", type=float, default=2.5,
help="车辆-障碍物警告 TTC 阈值(秒)")
parser.add_argument("--obstacle-critical-ttc", type=float, default=1.2,
help="车辆-障碍物临界 TTC 阈值(秒)")
parser.add_argument("--obstacle-collision-ttc", type=float, default=0.8,
help="车辆-障碍物碰撞 TTC 阈值(秒)")
# 归一化间隙阈值
parser.add_argument("--near-gap-normalized", type=float, default=0.8,
help="归一化间隙阈值,小于该值认为“接近”")
parser.add_argument("--contact-gap-normalized", type=float, default=0.15,
help="归一化间隙阈值,小于该值认为“接触”")
# 速度阈值
parser.add_argument("--speed-eps", type=float, default=2.0,
help="相对速度低于该值时忽略 TTC 计算(像素/秒)")
args = parser.parse_args()
cap = open_source(args.source)
if not cap.isOpened():
raise RuntimeError(f"Cannot open source: {args.source}")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 1:
fps = 25.0
print("=" * 70)
print("Collision Detector")
print("=" * 70)
print(f"Input : {width} x {height}")
print(f"FPS : {fps:.2f}")
print(f"Model : {args.model}")
print(f"Mode : {args.target_mode}")
print(f"Motion Validation: {'Enabled' if not args.no_motion_validation else 'Disabled'}")
print("=" * 70)
writer = None
if not args.no_save:
writer = cv2.VideoWriter(args.output, cv2.VideoWriter_fourcc(*"mp4v"),
fps, (width, height))
if not writer.isOpened():
raise RuntimeError(f"Cannot open output: {args.output}")
detector = CollisionDetector(
model_path=args.model,
fps=fps,
conf=args.conf,
imgsz=args.imgsz,
tracker=args.tracker,
target_mode=args.target_mode,
warning_frames=args.warning_frames,
critical_frames=args.critical_frames,
collision_frames=args.collision_frames,
release_frames=args.release_frames,
cooldown_frames=args.cooldown_frames,
use_motion_validation=not args.no_motion_validation,
angular_threshold=math.radians(args.angular_threshold),
decel_threshold=args.decel_threshold,
contact_iou=args.contact_iou,
near_gap_pixels=args.near_gap,
motion_strict=args.motion_strict,
show_detail=args.show_detail,
ignore_geometry=args.ignore_geometry,
vehicle_warning_ttc=args.vehicle_warning_ttc,
vehicle_critical_ttc=args.vehicle_critical_ttc,
vehicle_collision_ttc=args.vehicle_collision_ttc,
obstacle_warning_ttc=args.obstacle_warning_ttc,
obstacle_critical_ttc=args.obstacle_critical_ttc,
obstacle_collision_ttc=args.obstacle_collision_ttc,
near_gap_normalized=args.near_gap_normalized,
contact_gap_normalized=args.contact_gap_normalized,
motion_confirm_frames=args.motion_confirm_frames,
speed_eps=args.speed_eps,
)
frame_index = 0
t0 = time.time()
last_print = time.time()
processed = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_index += 1
timestamp = frame_index / fps
output, state, pair_results = detector.process(frame, timestamp)
if writer is not None:
writer.write(output)
if args.show:
cv2.imshow("Collision Detector", output)
key = cv2.waitKey(1) & 0xFF
if key == 27:
break
processed += 1
if time.time() - last_print > 1.0:
elapsed = time.time() - t0
current_fps = processed / elapsed if elapsed > 0 else 0
print(f"\rFrame={frame_index:6d} FPS={current_fps:6.2f} State={state:10s}",
end="", flush=True)
last_print = time.time()
finally:
cap.release()
if writer is not None:
writer.release()
cv2.destroyAllWindows()
print()
print("=" * 70)
print("Finished")
print(f"Output: {args.output}")
print("=" * 70)
if __name__ == "__main__":
main()
1. 不再只看“距离近”,而是看“时间还剩多少”
以前只看两车之间的像素距离(gap),距离一近就报警。但正常跟车时,两车可能一直保持很近,但相对速度很小,并不会撞上。
现在我重点看 TTC(Time To Collision,距离碰撞还剩多少秒)。只有 TTC 很小(比如不到 0.5 秒)才认为危险。跟车时哪怕距离近,只要相对速度小,TTC 就很大,就不会报警。
2. 把“接触”的标准提高了
以前两车边缘只要有点近(比如间隙小于车身尺寸的四分之一)就算“接触”,很容易达到。现在要求明显更近(比如间隙小于车身的十分之一),或者确实有很大重叠才认为接触。避免轻微靠近就被当成碰撞。
3. 运动学突变需要“连续出现”才算数
以前单帧检测到角速度或减速度突变,就认为是碰撞征兆。但单帧可能因为跟踪抖动产生假信号。现在要求连续好几帧都出现突变才确认,避免偶然抖动引起的误报。
4. 不同目标类型分开对待
车辆撞障碍物(比如锥桶、护栏)和车辆撞车辆,物理表现不同。以前用同一套标准,导致对障碍物也误报。现在区分:
- 车辆-车辆:需要角速度和减速度同时突变,或者 TTC 极低才报警。
- 车辆-障碍物:只要求减速度突变(因为撞静止物时方向变化小),或者 TTC 很低。
这样更符合实际。
5. 状态升级逻辑修正
以前一旦满足碰撞候选,警告、临界、碰撞的计数器会同时增加,导致很快跳到最高级“碰撞”。现在改为:满足碰撞候选时只增加碰撞计数,其他计数清零。也就是说,要连续几帧都明确满足碰撞条件才会报碰撞,不会因为一帧的误判就迅速升级。
6. 忽略极低速度的 TTC
当两车相对速度非常小时(比如都停着或几乎同速),TTC 计算会变得不稳定,可能算出虚假的小值。现在规定:相对速度低于某个阈值就不计算 TTC,避免这种噪声。
问题1:如果相对速度是差不多的,不认为是碰撞?
你的意思是你看了相对速度,如果相对速度是差不多的,不认为是碰撞?
是的,你的理解基本正确,但更准确地说:我们不仅看相对速度,还结合了当前距离,计算出一个“碰撞时间”(TTC,Time To Collision)。这个时间才是关键。
通俗解释:
假设你开车跟着前车,两车距离 10 米。
- 如果你们速度一样(相对速度≈0),那么永远不会撞上(TTC 无穷大),所以即使距离很近也不报警。
- 如果前车突然刹车,你的速度比它快,相对速度增大,比如每秒接近 5 米,那么 10 米的距离,碰撞时间就是 2 秒(TTC=2s)。如果这个时间小到一定程度(比如小于 0.5 秒),就非常危险,系统才会报警。
所以判断逻辑是:
危险程度 = 距离 / 相对速度 = TTC
- TTC 大 → 安全
- TTC 小 → 危险
跟车时误报,往往是因为以前系统只看距离(距离近就报警),忽略了相对速度。现在我们用 TTC 替代单纯的“距离近”,这样跟车时相对速度小,TTC 大,就不会误报。
另外,我们还增加了其他条件:
- 运动学突变:比如急刹车或急打方向,即使 TTC 还没降到极低,也能辅助确认碰撞风险。
- 目标类型区分:车辆撞静止障碍物时,障碍物速度为零,相对速度就是车辆自己的速度,TTC 会很小,系统能检测到。
- 连续多帧确认:避免单帧抖动引起的误报。
所以核心是:不是简单看相对速度是否相同,而是用“距离除以相对速度”得到的时间来判断紧迫性,这样既不会错过真实碰撞,也能大幅减少跟车误报。
问题2:如何调参?
一、先明确:误报发生在哪个状态?
系统有四个状态:WARNING、CRITICAL、COLLISION。误报可能出现在任何一个状态。你需要先观察视频,确定误报的类型。
- 如果频繁报 WARNING:说明系统认为“有碰撞风险”太容易触发。
- 如果频繁报 CRITICAL:说明系统认为“危险接近”太容易触发。
- 如果频繁报 COLLISION:说明系统认为“已碰撞”太容易触发。
不同状态对应的参数不同,所以先定位问题。
二、根据误报类型调整参数
1. 误报 WARNING 太多
WARNING 的触发条件是:
geom_close and approaching and ttc < warn_ttc
即:几何上接近(geom_close) + 正在靠近(approaching) + TTC 小于警告阈值。
可调整参数:
--vehicle-warning-ttc:调小(如从 1.5 降到 0.8),要求更紧迫才警告。--obstacle-warning-ttc:调小(如从 2.5 降到 1.5)。--near-gap-normalized:调小(如从 0.8 降到 0.4),让“接近”要求更严格。--speed-eps:调大(如从 2 调到 5),忽略低速相对运动,减少低速跟车时的误报。
典型场景: 正常跟车时经常报 WARNING,可以调小 TTC 阈值,或提高 speed-eps。
2. 误报 CRITICAL 太多
CRITICAL 的触发条件是:
geom_close and approaching and ttc < crit_ttc
即比 WARNING 更紧迫,TTC 更小。
可调整参数:
--vehicle-critical-ttc:调小(如从 0.8 降到 0.4)。--obstacle-critical-ttc:调小(如从 1.2 降到 0.6)。--critical-frames:调大(如从 3 调到 5),需要更多连续帧确认。
典型场景: 在车流中,两车距离较近且相对速度较大时可能误报 CRITICAL,可提高临界确认帧数或降低 TTC 阈值。
3. 误报 COLLISION 太多
COLLISION 的触发条件是:
geom_contact and (ttc < coll_ttc or motion_confirmed)
即:几何接触(geom_contact) + 极低 TTC 或 运动学突变确认。
可调整参数:
--contact-iou:调大(如从 0.05 调到 0.2 或 0.3),要求框有真实重叠。--contact-gap-normalized:调小(如从 0.15 调到 0.05 或 0.0),禁用间隙判定,只靠 IoU。--vehicle-collision-ttc:调小(如从 0.5 降到 0.2)。--obstacle-collision-ttc:调小(如从 0.8 降到 0.3)。--motion-confirm-frames:调大(如从 2 调到 4),要求运动学突变连续多帧。--collision-frames:调大(如从 3 调到 6),碰撞候选需持续更久。--decel-threshold:调大(如从 150 调到 300),减速度突变更难满足。--angular-threshold:调大(如从 15 调到 30),角速度突变更难满足。
典型场景: 你遇到的情况:目标丢失后错误关联到其他目标,或轻微重叠就被判碰撞。此时应重点提高 contact-iou 和降低 contact-gap-normalized,并增加 motion-confirm-frames。
三、调试技巧
1. 开启 --show-detail
运行时加上 --show-detail,左上角会显示每对目标的几何接触情况、运动学突变情况、TTC 等,帮助你判断为什么触发。
例如:
python collision_detector.py --show-detail
观察误报时,Geom 和 Motion 的具体数值,就能知道是哪个条件满足导致误报。
2. 临时打印关键变量
在 PairStateMachine.update 中临时添加打印,输出:
print(f"pair {id1}-{id2}: iou={iou:.3f}, norm_gap={normalized_gap:.3f}, ttc={ttc:.2f}, motion_hit={motion_hit}, event={event}")
这样可以在终端看到每一帧的判断依据。
3. 逐步调整,不要一次改太多
一次只调整 1-2 个参数,观察效果,再继续调整。
四、常见场景的推荐调整
| 场景 | 主要误报原因 | 建议调整 |
|---|---|---|
| 正常跟车 | 距离近但相对速度小 | 调小 vehicle-warning-ttc,调大 speed-eps |
| 车辆并排行驶 | 横向距离近但不会碰撞 | 调小 near-gap-normalized,调大 contact-iou |
| 车辆与静止障碍物 | 障碍物被误检或距离计算不稳定 | 调大 obstacle-warning-ttc 的确认帧数,调小 contact-gap-normalized |
| 目标短暂丢失导致错误关联 | 跟踪断裂 | 调大 motion-confirm-frames,调大 collision-frames,提高 contact-iou |
| 检测框抖动导致运动学突变误判 | 减速度/角速度噪声 | 调大 decel-threshold,调大 angular-threshold |
五、记住:调整的黄金法则
先收紧几何条件,再收紧时间条件,最后收紧运动学条件。
因为几何条件是基础,如果两个目标根本没有重叠或极近,后面的条件再宽松也不会误报(除非运动学突变单独触发,但可以通过 motion-confirm-frames 控制)。
所以优先调整:
contact-iou和contact-gap-normalizedvehicle-*-ttc和obstacle-*-ttcmotion-confirm-frames和collision-frames
按照这个顺序调整,通常能快速减少误报。
六、何时需要放宽参数?
如果真实碰撞没有被检测到(漏报),说明条件过严,需要反向调整:
- 降低
contact-iou要求 - 增大
contact-gap-normalized - 减小
collision-frames或motion-confirm-frames - 减小
decel-threshold或angular-threshold
但要注意平衡,避免再次引入误报。

485


被折叠的 条评论
为什么被折叠?



