# Heading Debug 时序分析方案

## 1. 背景与目标

当前 `merge_json`、`roi0`、`roi1` 结果文件中已经保存了 `heading_debug` 字段，包含：

- `yaw_bin`
- `yaw_delta`
- `rot_y_decoded`
- `yaw_probs`

这些字段可以帮助区分 heading 不稳定到底来自：

1. `yaw_bin` 分类抖动
2. `yaw_delta` 回归不稳
3. `+pi / -pi` 附近的角度回绕假跳变
4. ROI0 / ROI1 分支选择不一致
5. 遮挡、截断、远距离带来的朝向歧义

本文档给出一套基于 `heading_debug` 的时序分析方案，目标是回答以下问题：

- heading 不稳定主要发生在哪些类别、距离段、ROI 分支中
- 不稳定的主因是 bin 选错，还是 delta 抖动
- merged 结果的不稳定是来自模型本身，还是来自 ROI merge / NMS 选择
- 是否存在 `+pi / -pi` 表达带来的假性跳变

## 2. 推荐输入数据

建议优先使用以下结果作为分析输入：

- `merge_json/*.json`
- `roi0/*.json`
- `roi1/*.json`
- `track_objects.py` 输出的 tracking 结果，如果已有 `track_id`
- `frame_timestamps_interp.csv`，如果需要按真实时间间隔分析

如果已经走过 `tools/temporal_analysis/track_objects.py`，则推荐直接在 tracking 结果基础上分析，因为其中通常已经补充了：

- `track_id`
- `frameId`
- `timestamp`
- `sub_cls`

如果暂时没有 `track_id`，也可以先做 frame-level 分析，或者用相邻帧 IoU / 3D center 最近邻做临时关联。

## 3. `heading_debug` 字段解释

### 3.1 原始字段

每条 detection 中新增：

```json
"heading_debug": {
  "yaw_bin": 2,
  "yaw_delta": 0.137,
  "rot_y_decoded": -1.434,
  "yaw_probs": [0.01, 0.08, 0.91, 0.03]
}
```

含义如下：

- `yaw_bin`: 当前选中的 orientation bin，取值为 `0/1/2/3`
- `yaw_delta`: 当前选中 bin 内的 residual
- `rot_y_decoded`: 按 `yaw_bin + yaw_delta` 解码后的相机系朝向角
- `yaw_probs`: 4 个 bin 的 sigmoid 分数

### 3.2 注意事项

- `rot_y_decoded` 与 `xyzlhwyaw[6]` 都是相机系 yaw，单位是弧度，可以直接比较。
- `xyzlhwyaw_ego[6]` 在当前脚本中是角度制 `deg`，不要和前两者直接混用。
- `yaw_probs` 是对 4 个 bin 分别做 sigmoid 得到的分数，不是 softmax 概率。
  - 不应使用“4 个值求和为 1”的理解。
  - 更应关注最大分数、次大分数以及它们的差值。

## 4. 数据整理建议

建议先把 JSON 扁平化成一张分析表，每行对应一条 detection。至少保留以下字段：

- `case_name`
- `image_name`
- `frameId`
- `timestamp`
- `track_id`，如果有
- `type` / `class_id` / `sub_cls`
- `score`
- `roi_id`
- `box2d`
- `xyzlhwyaw`
- `face_cls`
- `cut_cls`
- `heading_debug.yaw_bin`
- `heading_debug.yaw_delta`
- `heading_debug.rot_y_decoded`
- `heading_debug.yaw_probs`

建议继续派生以下分析字段：

- `x1, y1, x2, y2`
- `box_w = x2 - x1`
- `box_h = y2 - y1`
- `box_area = box_w * box_h`
- `depth_z = xyzlhwyaw[2]`
- `yaw_final = xyzlhwyaw[6]`
- `p1 = max(yaw_probs)`
- `p2 = second_max(yaw_probs)`
- `margin = p1 - p2`
- `num_pos_bins = count(yaw_probs > 0.5)`

### 4.1 推荐扁平表输出

建议至少落两份中间结果：

- `heading_debug_flat.csv`
- `heading_debug_track_summary.csv`

其中：

- `heading_debug_flat.csv` 用于逐 detection 排查
- `heading_debug_track_summary.csv` 用于 track 级汇总和排序

## 5. 第一轮：做全局统计总览

在看单条 case 前，先做整体统计，回答“不稳定集中出现在哪里”。

建议统计以下分布：

- 各类别的 `yaw_bin` 分布
- 各类别的 `margin` 分布
- 各类别在不同 `depth_z` 范围下的 `margin` 分布
- 各类别在不同 `box_area` 范围下的稳定性指标
- `roi_id = 0/1` 的稳定性差异
- `face_cls` / `cut_cls` 不同取值下的 heading 波动大小

重点观察：

- 不稳定是否集中在远距离、小框、低分样本
- 不稳定是否明显偏向某个 ROI 分支
- 不稳定是否集中在某个类别或某种可见面

## 6. 第二轮：做时序稳定性分析

如果具备 `track_id`，建议以 track 为基本单位分析。

### 6.1 必须做角度 wrap

直接做 `yaw_t - yaw_t-1` 会把 `+179° -> -179°` 误判成大跳变，因此角度差必须按环形处理：

```python
import numpy as np

def wrapped_angle_diff(a, b):
    return np.arctan2(np.sin(a - b), np.cos(a - b))
```

其中：

- `a`、`b` 为弧度制 yaw
- `wrapped_angle_diff(a, b)` 输出范围约为 `[-pi, pi]`

### 6.2 推荐 track 级指标

对每条 track 统计：

- `track_len`
- `median(|Δyaw|)`
- `p95(|Δyaw|)`
- `max(|Δyaw|)`
- `yaw_bin_switch_count`
- `yaw_bin_switch_rate`
- `median(|Δyaw_delta|)`
- `max(|Δyaw_delta|)`
- `low_margin_ratio`，例如 `margin < 0.1` 的占比
- `roi_switch_count`
- `face_cls_switch_count`
- `cut_cls_switch_count`

其中：

- `Δyaw` 使用 wrap 后角度差
- `Δyaw_delta` 只建议在相邻帧 `yaw_bin` 相同的条件下统计

### 6.3 推荐时序样本筛选方式

可以先筛出最不稳定的 track：

- `max(|Δyaw|)` 最大的 Top-K
- `p95(|Δyaw|)` 最大的 Top-K
- `yaw_bin_switch_rate` 最高的 Top-K

优先人工查看这些 track，对定位问题最有效。

## 7. 根因归类规则

可以根据 `heading_debug` 的表现把不稳定样本归成以下几类。

### 7.1 `yaw_bin` 抖动型

特征：

- 相邻帧 `yaw_bin` 频繁切换
- 同时 `margin` 较小，例如 `< 0.1`
- 或 `num_pos_bins >= 2`

说明：

- 问题更偏向 bin 分类头不稳定
- 常见于朝向模糊、遮挡、远距离、小目标

### 7.2 `yaw_delta` 抖动型

特征：

- 相邻帧 `yaw_bin` 基本不变
- 但 `|Δyaw_delta|` 很大
- `margin` 还比较稳定

说明：

- bin 选对了，但 bin 内 residual 回归不稳定
- 更像回归头噪声，而不是分类歧义

### 7.3 `+pi / -pi` 回绕假跳变型

特征：

- 样本集中在 `yaw_bin = 3` 或接近 `±pi`
- 原始 yaw 数值正负跳变明显
- 但 wrap 后 `|Δyaw|` 实际并不大

说明：

- 这类更多是角度表示问题，不一定是真正的 heading 不稳

### 7.4 ROI 敏感型

特征：

- ROI0 和 ROI1 分支对同一目标给出的 `heading_debug` 差异较大
- merged 结果的不稳定伴随 `roi_id` 切换

说明：

- 问题可能来自 crop 视角差异、ROI 边界、分支选择或 cross-ROI merge

### 7.5 可见性 / 朝向歧义型

特征：

- heading 跳变时伴随 `score` 低、框小、距离远
- 同时 `face_cls` 或 `cut_cls` 也在变化

说明：

- 更像输入信息不足导致的朝向歧义，而非单纯模型数值抖动

### 7.6 180 度翻转型

特征：

- 相邻帧 `|Δyaw|` 接近 `pi`
- 同时 `face_cls` 可能发生 `front <-> tail` 切换

说明：

- 需要重点怀疑前后向语义混淆
- 通常不只是小噪声，而是 orientation 语义层面的错误

## 8. ROI0 / ROI1 / merged 的分层分析建议

强烈建议不要只看 `merge_json`，而是同时对比：

- `roi0/*.json`
- `roi1/*.json`
- `merge_json/*.json`

可以按以下逻辑判断问题来源：

### 情况 1：ROI0 和 ROI1 都稳定，但 merged 不稳定

说明：

- 更可能是 cross-ROI NMS / merge / 分支选择问题

### 情况 2：ROI0 和 ROI1 本身差异很大

说明：

- 更可能是模型对不同 crop 视角敏感
- 需要结合 ROI 边界、目标位置、可见面一起看

### 情况 3：ROI0、ROI1、merged 都不稳定

说明：

- 更可能是模型本身 heading 头不稳
- 或目标本身确实难判定

## 9. 如果有 GT，建议把误差拆成“bin 错”还是“delta 错”

如果测试数据带朝向标注，可以把 GT yaw 也编码成 `gt_bin + gt_delta`，编码规则与后处理保持一致：

- `[-pi/4, pi/4)` -> `bin 0`, `delta = yaw`
- `[pi/4, 3pi/4)` -> `bin 1`, `delta = yaw - pi/2`
- `[-3pi/4, -pi/4)` -> `bin 2`, `delta = yaw + pi/2`
- 其他区域 -> `bin 3`
  - `yaw > 0` 时 `delta = yaw - pi`
  - `yaw <= 0` 时 `delta = yaw + pi`

然后分别统计：

- `pred_bin == gt_bin` 的比例
- 在 `pred_bin == gt_bin` 条件下，`|pred_delta - gt_delta|`
- 最终 `|wrapped(pred_yaw - gt_yaw)|`

这样可以直接判断：

- 如果主要问题是 `pred_bin != gt_bin`，则偏向分类头问题
- 如果 `pred_bin` 大多正确，但 `delta` 偏差大，则偏向回归头问题

## 10. 推荐优先产出的图表

建议优先做以下 4 类图：

### 10.1 Track yaw 时序图

横轴：

- `frameId` 或 `timestamp`

纵轴：

- `yaw_final` 或 `rot_y_decoded`

同时：

- 用颜色标出 `yaw_bin`
- 标记 `roi_id` 切换点
- 标记 `margin` 低于阈值的时刻

用途：

- 最直观地看 heading 跳变和 bin 切换的关系

### 10.2 `|Δyaw|` vs `margin` 散点图

横轴：

- `margin`

纵轴：

- `|Δyaw|`

用途：

- 判断大跳变是否集中在低置信 bin 竞争区域

### 10.3 `yaw_bin(t-1) -> yaw_bin(t)` 转移矩阵

用途：

- 看哪些 bin 最容易互相跳转
- 尤其关注 `0 <-> 3`、`1 <-> 2`、`front / tail` 相关模式

### 10.4 分桶稳定性统计图

按以下维度分桶：

- `class`
- `roi_id`
- `depth_z`
- `box_area`
- `face_cls`

统计每个桶里的：

- `median(|Δyaw|)`
- `p95(|Δyaw|)`
- `yaw_bin_switch_rate`
- `low_margin_ratio`

用途：

- 快速定位最差场景

## 11. 推荐执行顺序

建议按以下顺序推进：

### 第一步：做全量统计

先找出不稳定最严重的：

- 类别
- ROI
- 距离段
- 框尺度区间

### 第二步：抽 Top-K 最差 track

建议优先抽：

- `max(|Δyaw|)` 最大的 Top-K
- `yaw_bin_switch_rate` 最高的 Top-K
- `low_margin_ratio` 最高的 Top-K

### 第三步：逐条排查根因

每条 track 同时看：

- `yaw_bin`
- `yaw_delta`
- `yaw_probs`
- `margin`
- `roi_id`
- `face_cls`
- `cut_cls`
- `score`
- `depth_z`

### 第四步：归因总结

把问题归类到以下一种或几种：

- `yaw_bin` 抖动型
- `yaw_delta` 抖动型
- 角度回绕假跳变型
- ROI 敏感型
- 可见性歧义型
- 180 度翻转型

## 12. 最终建议输出物

建议最终沉淀以下分析产物：

- `heading_debug_flat.csv`
- `heading_debug_track_summary.csv`
- `heading_debug_bad_tracks.csv`
- `fig_track_yaw_series/` 目录
- `fig_margin_vs_dyaw.png`
- `fig_bin_transition_matrix.png`
- `fig_stability_by_depth.png`
- `fig_stability_by_roi.png`
- `heading_debug_analysis_report.md`

其中 `heading_debug_analysis_report.md` 建议包含：

- 哪些类别最不稳定
- 哪些场景最不稳定
- 主要根因归类占比
- 是否更偏向 bin 问题还是 delta 问题
- 是否存在明显 ROI merge 问题

## 13. 实施时的关键注意点

### 13.1 单位统一

- `rot_y_decoded`、`xyzlhwyaw[6]` 为弧度
- `xyzlhwyaw_ego[6]` 为角度

做差前必须统一单位。

### 13.2 不要直接看未 wrap 的 yaw 差值

所有时序稳定性指标都建议基于 wrap 后角度差计算。

### 13.3 `yaw_probs` 不要当 softmax 使用

它们是 4 个独立 sigmoid 分数，建议用：

- 最大值 `p1`
- 次大值 `p2`
- 差值 `margin`
- 是否多个分数同时偏高

而不是看其和是否为 1。

### 13.4 merged 和 ROI 分支要分开分析

如果只看最终 merged 结果，容易把问题都归到模型本身。实际上很多问题可能来自：

- ROI 视角差异
- ROI 边界裁切
- cross-ROI NMS / merge 策略

### 13.5 先做统计，再做人工看图

不要一开始就逐条翻 case。先做分布和排序，效率更高。

## 14. 一句话结论

基于 `heading_debug` 的核心分析思路是：

- 先把 detection 扁平化并补齐 track / timestamp
- 再用 wrap 后角度差做时序稳定性统计
- 再结合 `yaw_bin`、`yaw_delta`、`margin`、`roi_id`、`face_cls` 做根因归类
- 最后区分问题到底来自 bin 分类、delta 回归、角度表达、ROI 分支还是目标本身歧义
