Personal Knowledge Base

A long-term research and learning notebook for posts, notes, papers, projects, and research directions.

Skip to content
Back to RiskAffordance overview

DoTA 校准

回忆合成配对的核心动作:按事故类型在 H 场上放一个风险核,需要四个参数:

76 min read

校准的对象

回忆合成配对的核心动作:按事故类型在 H 场上放一个风险核,需要四个参数:

锚点方位(核放哪个方向)  ← 例:rear_end 放正前
锚点距离(核放多远)      ← 例:13~15m
核宽度 σ(高斯核多宽)
核幅度 a(核多亮)

没有校准,这四个数只能拍脑袋。DoTA 校准 = 用真实事故数据把这四个参数的取值分布统计出来,让合成规则照着真实分布采样。注意定位:DoTA 提供的是参数分布,不是逐样本监督——训练配对仍然是程序合成的,真实数据只负责"量尺寸"。

原材料:没用视频,用的是标注 JSON

一个务实的关键决策:DoTA 全量原始帧 55GB 没有下载。实际用的是 DoTA 仓库自带的逐视频标注 JSON(4677 个,含全帧 labels)。官方预提取的 bbox tracks 下载链接失效(GitHub issue #43,无人修复),所以直接从原始标注解析——绕开失效链接,不碰大文件。

每个 JSON 里每帧是一条记录:frame_id / image_path / accident_id / accident_name / objects,objects 里是每个物体的 obj_track_id / bbox / category / trunc(截断标志)。

数据探查:三个发现直接重塑了方案

动手统计之前先做了只读探查,这一步改变了整个设计:

  1. 发现 1:bbox 只存在于事故区间内。原以为能拿到"事故前几秒"的物体轨迹来统计逼近动态,实际验证到叶子字段后发现:全数据集事故前窗口一个框都没有(4326 个视频只在事故期有框,290 个完全无框直接排除)。之前"labels 每帧非空"造成了误判——每帧都有 dict,但事故前的 dict 里 objects 是空的。教训入库:数据探查要验证到叶子字段,"条目非空"≠"有内容"。

  2. 发现 2:统计窗口被迫改为 onset 窗口。事故前窗口不存在,统计口径只能定为:

    onset 窗口 = [anomaly_start, anomaly_start + 10帧) ≈ 事故发生后的头 1 秒

    同时保留 onset/unfolding(事故发展期)分层输出,并明文禁止"事故前 TTC 窗口"这种表述——窗口语义决定统计能回答什么问题,口径必须诚实。

  3. 发现 3:accident_id 是类别编号,不是参与方标记。这决定了只能做"这类事故里危险出现在哪"的类别级统计,做不了"两个参与方谁撞谁"的交互级分析。

从 2D 框到 3D 锚点:针孔投影 + 接地点法

DoTA 是单目行车记录仪视频,标注是 2D 框。要得到"核放多远、哪个方位",需要把 2D 框变回 3D 位置:

第 1 步:取 bbox 的"接地中点"(框底边中点——车和地面接触的位置)
第 2 步:针孔相机模型反投影:像素点 → 从相机出发的一条射线
第 3 步:射线与地面(z = 路面平面)求交 → 得到物体的 3D 位置
第 4 步:换算到 ego 系 → 方位角 bearing = atan2(横向, 前向)、距离 d

为什么用接地点而不是框中心?框中心悬在半空,投影回去深度不定;接地点贴地,地面假设给出唯一的深度解——单目测距的标准技巧。

0.5~40m 有效性门限:太近的框(<0.5m,多为畸变/标注噪声)和太远的框(接近地平线,投影误差爆炸式放大、距离长尾到几百米)都剔除。实测 onset 窗口有 294 条投影无效被过滤。

逐类统计:类别映射与分布提取

DoTA 的事故类别映射到本项目的 9 类(映射表 dota_type_map.yaml),有效样本 4616 条(61 条 ignore 剔除):

turning 1676 / lateral 714 / moving_ahead_or_waiting 660 / oncoming 475
leave_to_left 366 / leave_to_right 353 / pedestrian 97 / start_stop 94 / obstacle 91
(unknown 90 → 排除)

对映射后的每一类,在 onset 窗口内统计所有样本的锚点,得到经验分布(不是单个均值,而是分布——采样时要从分布里抽,保留真实的离散度)。

这里出了全项目最重要的一个语义修正:moving_ahead_or_waiting → rear_end 的锚点统计出来在正前方 ~0.046 rad——真实语义是"ego 追尾前车(前车急刹/静止)",而项目原设定是"被追尾"(锚点正后方 −π)。方位整整差了 180°。

拍板原则:类型语义以真实数据为准。这一行统计结果直接翻转了 rear_end 的先验语义,并引发了后面那一长串同步改造(模板改写、缓存重编码、affinity 漏改的雷)。

另有两类被标 low_confidence:leave_to_left/rightstart_stop——DoTA 语义和本项目类型对不齐(语义不匹配),映射表注释里如实标明,统计值降级使用。

幅度反推:一次光荣的失败

四个参数里,方位/距离/σ 都是几何量,投影就能算。幅度 a 不行——它来自公式 a = exp(-d/20) × clamp(1−TTC/3),需要 TTC,TTC 需要 closing speed,closing speed 需要帧间运动。

团队尝试用 onset 窗口内的框运动反推 closing speed,进而反推幅度。结果:除 oncoming 外,所有类的幅度中位数 = 0。原因很物理:onset 窗口是事故已经开始的 1 秒——碰撞正在发生或车辆已经停滞,closing speed 趋近于零("closing 停滞"),TTC≈∞,TTC 因子直接归零。幅度公式的物理前提在这个窗口里不成立。

处置方式是这个项目诚实纪律的典范:

  • 不强行使用零中位数(那会把 rear_end 等类的核直接熄灭——修复前已观察到 rear_end 校准核近乎归零的图证);
  • 给 8 个类打上 amplitude_degenerate 标记(turning/lateral/moving_ahead/leave_to_*/pedestrian/start_stop/obstacle),只有 oncoming/head_on 保留校准幅度;
  • 退化类的规则:幅度退回默认值,但距离项仍用校准值——能校准的校准,校准不了的如实退回,逐类计数登记。

口径一句话:"归零"本身就是结论——它定量证明了"事故期统计做不了幅度校准",写进答辩素材比编一个数字硬气得多。

注入合成管线:pair_gen 的 calibration 分支

统计产物落盘为 dota_stats.yaml(onset/unfolding 分键)后,pair_gen 新增 calibration 采样分支,消费方式:

生成每个合成配对时:
  按事故类型查 dota_stats.yaml 的经验分布
  → 锚点方位/距离/σ/幅度 从分布中采样(np.random.default_rng(seed),确定性可复现)
  → 幅度退化类走 fallback:默认幅度 + 校准距离(严格 fallback,逐类计数)

两个工程细节:

  • 校准锚点距离实测范围 2.3139.69m——和 0.540m 投影门限吻合,说明分布没有被截断污染;
  • fallback 类是内置控制组:cut_in_left/rightcyclist 三类走 fallback,要求新旧版本逐字节一致(train 1473/1473、val 350/350 对拍通过)——这保证了"校准改动只影响该影响的类",管线无污染。这个控制组设计后来在消融对照里反复立功(差异必须由模型行为解释,解释不了就是数据泄漏信号)。

全量产物:data/synthetic_pairs_calib_v2/,train 4419 对(9 类各 491)/ val 1047 对。

校准的能力边界(诚实总结,答辩口径)

参数校准结果原因
锚点距离✅ 主要收益接地点投影可靠
核宽度 σ✅ 按类型分层与 bbox 尺寸相关
锚点方位⚠️ 有限前向行车记录仪是物理盲区——摄像头只看前方,被追尾类永远拍不到后车,方位天然聚集在 0° 附近
核幅度❌ 8 类退化onset 窗口 closing 停滞,TTC 因子物理失效

一句话:校准修得了距离与 σ,修不了方位与幅度——这不是执行不力,是数据源(前向单目 + 只标事故期)的结构性边界。