车辆碰撞1——角速度突变 + 急减速

一、前言

我去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 大,就不会误报。

另外,我们还增加了其他条件:

  1. 运动学突变:比如急刹车或急打方向,即使 TTC 还没降到极低,也能辅助确认碰撞风险。
  2. 目标类型区分:车辆撞静止障碍物时,障碍物速度为零,相对速度就是车辆自己的速度,TTC 会很小,系统能检测到。
  3. 连续多帧确认:避免单帧抖动引起的误报。

所以核心是:不是简单看相对速度是否相同,而是用“距离除以相对速度”得到的时间来判断紧迫性,这样既不会错过真实碰撞,也能大幅减少跟车误报。

问题2:如何调参?

一、先明确:误报发生在哪个状态?

系统有四个状态:WARNINGCRITICALCOLLISION。误报可能出现在任何一个状态。你需要先观察视频,确定误报的类型。

  • 如果频繁报 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 控制)。

所以优先调整:

  1. contact-ioucontact-gap-normalized
  2. vehicle-*-ttcobstacle-*-ttc
  3. motion-confirm-framescollision-frames

按照这个顺序调整,通常能快速减少误报。


六、何时需要放宽参数?

如果真实碰撞没有被检测到(漏报),说明条件过严,需要反向调整:

  • 降低 contact-iou 要求
  • 增大 contact-gap-normalized
  • 减小 collision-framesmotion-confirm-frames
  • 减小 decel-thresholdangular-threshold

但要注意平衡,避免再次引入误报。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值