Files

374 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 锥桶避障功能 — 中线变形方案
## 1. 核心思路
**不改舵机代码,直接修改 `mid_line[]` 数组。** 在锥桶附近把中线"推开"一个凸起,后续巡线逻辑(deviation → deadband → servo)原样工作,车自然跟着变形后的中线绕开锥桶。
锥桶消失后进入**保持衰减**阶段(`cone_hold_frames` 帧内变形量线性衰减),随后进入**回弹**阶段(`cone_return_frames` 帧内朝锥桶方向反推,帮助车回到赛道中央)。
### 1.1 原始中线快照 (`mid_line_raw`)
避障变形会原地修改 `mid_line[]`,导致方向判断被污染。为解决此问题,在 `image_main()` 之后立即保存一份快照:
```
image_main() → mid_line[] = 赛道真实中线
mid_line_raw = mid_line → 快照 (60 个 int, 240 字节, 每帧复制)
lidar_avoid_process() → 修改 mid_line[] (lidar 推力)
cone_detect_and_deform() → 修改 mid_line[] (锥桶推力)
steering_update() → 读 mid_line[] (变形后) 控制舵机
```
| 变量 | 含义 | 来源 | 被修改 | 用途 |
|------|------|------|--------|------|
| `mid_line_raw[]` | 赛道在哪 | `image_main()` 每帧重算 | 否 | 方向判断 |
| `mid_line[]` | 车该往哪走 | `mid_line_raw` + 避障推力 | 是 | 舵机控制 |
**所有方向判断(锥桶在赛道左侧还是右侧、回弹方向)均基于 `mid_line_raw`**
避免变形后的中线导致方向翻转。
---
## 2. 坐标与边界过滤
```
模型空间 (raw_frame):
正常模式: 160×120 (MJPEG 1/4解码)
回退模式: 640×480
巡线空间映射:
cone_col = box.cx * line_tracking_width / raw_frame.cols (典型 0~79)
cone_row = box.cy * line_tracking_height / raw_frame.rows (典型 0~59)
换算速查 (正常模式 160×120 → 典型 80×60):
cone_col = cx / 2
cone_row = cy / 2
```
边界过滤 (cls=0, conf ≥ cone_thresh):
```
row_lt < 10 → 丢弃 (row 0~9 无可靠巡线数据)
row_lt >= line_tracking_height → 丢弃
left_line[row_lt] == -1 → 丢弃 (该行丢线)
right_line[row_lt] == -1 → 丢弃
col_lt < left_line[row_lt] → 丢弃 (赛道外)
col_lt > right_line[row_lt] → 丢弃 (赛道外)
```
---
## 3. 中线变形
### 3.1 方向
```
mid_at_cone = mid_line[cone_row]
cone_col < mid_at_cone → 锥在左 → push_mid 向右 (+)
cone_col > mid_at_cone → 锥在右 → push_mid 向左 (-)
direction = (cone_col < mid_at_cone) ? +1 : -1
```
### 3.2 变形区域
`start_row = max(10, cone_row - range)``cone_row`,共 (cone_row - start_row) 行:
```
row 0 ───────────────────────────────── 远
│ (不变形)
row 10 ─────────────────────────────────
│ start_row ────────────── t=0
│ 线性斜坡
│ foresee 行在这里看到部分偏移
│ cone_row ────────────── t=1
row 59 ───────────────────────────────── 近 (车前方)
```
### 3.3 数学
```
for row in [start_row, cone_row]:
t = (row - start_row) / (cone_row - start_row) // [0, 1]
push = t * cone_avoid_gain * (line_tracking_width / 2) * direction
mid_line[row] = original_mid_line[row] + push
mid_line[row] = clamp(mid_line[row],
left_line[row] + 2,
right_line[row] - 2)
```
**参数:**
- `cone_avoid_gain` (double, 0.3):归一化推离量。1.0 = 推到半幅赛道宽。默认 0.3 表示推到赛道 30% 宽度处。
- `cone_avoid_range` (int, 30):斜坡覆盖行数。必须足够大使 foresee 行感受到偏移。
---
## 4. 数字算例
**场景:** 直道,锥桶在赛道左侧。`lt_w=80, lt_h=60, cone_avoid_gain=0.3, cone_avoid_range=30`
```
原始数据:
cone_col=25, cone_row=50, mid_at_cone=40 (锥在左侧15列)
direction = +1 (推中线向右)
start_row = max(10, 50-30) = 20
half_w = 40
变形计算:
push_max = 0.3 * 40 * 1 = 12 (行50处的最大位移, 巡线空间列)
row=20 (start, t=0): mid_line = 40 + 0 = 40 → deviation = 40*2-80 = 0 → 舵机中位
row=25 (t=5/30=0.17): mid_line = 40 + 2.04 = 42 → deviation = 42*2-80 = +4 → 略右
row=35 (t=15/30=0.5): mid_line = 40 + 6.0 = 46 → deviation = 46*2-80 = +12 → 明显右转
row=50 (t=1.0): mid_line = 40 + 12 = 52 → deviation = 52*2-80 = +24 → 大幅右转
foresee=40 (显示空间) → check_row=40/2=20 (巡线空间):
mid_line[20] 未被变形 (start_row=20, t=0)
→ 舵机看不到! 需要减小 range 或增大 foresee
```
**问题暴露:** `foresee=40``check_row=20`,但 `start_row=20` 时 t=0 无偏移。
**修正:** 确保变形起点在 `check_row` 以上。两种方式:
1. 增大 `cone_avoid_range`(如 40),让 start_row 更低
2. 让斜坡向上延伸超过 start_row(即从 row=10 就开始)
**采用方案 2** 变形始终从 `row=10` 开始,`range` 参数控制斜坡的"陡峭度"而非范围。
---
## 5. 修正后的变形公式
```
range = cone_avoid_range // 陡峭度参数 (默认 30)
total_rows = cone_row - 10 // 实际跨度
for row in [10, cone_row]:
t_raw = (row - 10.0) / total_rows // 线性 [0, 1]
t = clamp(t_raw * (total_rows / range), // 用 range 缩放斜率
0.0, 1.0)
push = t * cone_avoid_gain * (line_tracking_width / 2) * direction
mid_line[row] = clamp(original_mid + push,
left_line[row] + 2,
right_line[row] - 2)
```
**效果(cone_avoid_range=30, cone_row=50, total_rows=40):**
```
total_rows/range = 40/30 = 1.33 ← 斜率因子 >1, 斜坡更陡
row=10: t_raw=0, t=0*1.33=0 → push=0
row=20: t_raw=0.25, t=0.25*1.33=0.33 → push=0.33*12=4.0 → deviation=+8 (刚好过死区)
row=35: t_raw=0.625,t=0.83 → push=10 → deviation=+20
row=50: t_raw=1.0, t=1.0 → push=12 → deviation=+24
```
foresee 行 (row=20) 收到 33% 的偏移,产生 +8px 偏差 → 刚好越过 deadband(8px) → 舵机开始右转。
**调参规则:**
- 增大 `cone_avoid_range` → 斜坡更缓 → foresee 行偏移更小 → 转向更柔和
- 减小 `cone_avoid_range` → 斜坡更陡 → foresee 行偏移更大 → 转向更猛
- 增大 `cone_avoid_gain` → 整体偏移量增大 → 转向更猛
---
## 6. 去抖机制
```
g_cone_frames: 连续检测计数
位置容忍 (动态):
row_tol = max(2, line_tracking_height / 12) 典型值 5
col_tol = max(3, line_tracking_width / 8) 典型值 10
首次出现: g_cone_frames = 1, 记录位置
位置接近 (容差内): g_cone_frames++, 更新位置
位置突变: g_cone_frames = 1, 更新位置 (重新开始计数)
未检测到: g_cone_frames = max(0, g_cone_frames - 1)
g_cone_confirmed = (g_cone_frames >= cone_min_frames)
```
**cone_min_frames 默认值为 1(单帧即确认)。** 原因:视觉巡线(`image_main()` 的 floodFill
对赛道上的锥桶响应极快——锥桶进入视野后 1~2 帧内车就开始转弯,锥桶随即离开视野。
模型检测的窗口天然只有 1~2 帧,多帧去抖门槛(如 3)永远达不到。
边界过滤(row 有效、赛道内、conf ≥ cone_thresh)已经提供了足够的误触发保护。
---
## 7. 保持衰减 + 回弹
锥桶消失后不会立刻恢复正常巡线,而是经历三个阶段:
```
确认 (g_cone_confirmed && cone_seen)
→ g_cone_hold_ctr = 1, g_cone_return_ctr = 0
→ 冻结位置: hold_src_row/col = cone_row/col
→ 记录回弹方向: 锥桶在中线左侧 → return_dir = -1 (回弹推左)
锥桶在中线右侧 → return_dir = +1 (回弹推右)
确认但不可见 (g_cone_confirmed && !cone_seen, 去抖衰减中)
→ g_cone_hold_ctr = 1 (保持重置, 但不覆盖冻结位置)
→ 使用 hold_src_row/col 继续避让变形
保持衰减 (!g_cone_confirmed, 1 ~ cone_hold_frames 帧)
→ g_cone_hold_ctr++
→ decay = 1.0 - hold_ctr / hold_frames (线性衰减到 0)
→ 中线变形量乘 decay → 逐渐恢复
回弹 (保持结束后 1 ~ cone_return_frames 帧)
→ g_cone_return_ctr++
→ 反方向推中线 (return_dir), 力度 = cone_return_gain
→ 斜坡范围 = cone_avoid_range × 1.2 (比避让略缓)
→ 覆盖全部有效行 (row 10 ~ lt_h-1), 不限于锥桶位置
→ decay = 1.0 - return_ctr / return_frames (线性衰减)
→ 回弹结束: 清零所有状态变量
```
**cone_is_slow() 在三个阶段中均返回 true**(确认 / 保持 / 回弹),但锥桶减速代码当前**已注释**。
### 7.1 已修复的 BUGhold_src_row 被 -1 覆盖导致回弹永远不执行
**问题**:锥桶消失后,由于去抖计数 `g_cone_frames` 逐帧 -1 衰减,`g_cone_confirmed`
在锥桶消失后仍保持 true 多帧(如锥桶可见 20 帧 → 消失后 confirmed 维持 18 帧)。
在这些帧中 `cone_detect_and_deform()` 内的局部变量 `cone_row = -1`(没找到锥桶),
但旧代码在 `if (g_cone_confirmed)` 块中无条件执行:
```cpp
// ★ 旧代码 (BUG):
if (g_cone_confirmed) {
g_cone_hold_ctr = 1;
g_cone_hold_src_row = cone_row; // cone_row = -1, 覆盖有效值!
g_cone_hold_src_col = cone_col; // 同上
g_cone_return_ctr = 0;
double ms = mid_line[cone_row]; // mid_line[-1] 越界访问!
g_cone_return_dir = (cone_col < ms) ? -1.0 : 1.0;
}
```
后果链:
1. `hold_src_row` 被 -1 覆盖 → 有效位置丢失
2. `mid_line[-1]` 越界访问 → 未定义行为
3. 后续所有阶段 `src_row = hold_src_row = -1``if (src_row <= 10) return` 提前退出
4. 保持变形 **从不执行**,回弹变形 **从不执行**
```
时间轴: 锥桶可见 20帧 去抖衰减 18帧 hold 15帧 return 30帧
避让变形: ████████████████████ (src_row=-1,退出) (同上,-1) (同上,-1)
保持变形: 从不执行 从不执行
回弹变形: 从不执行
```
**修复**:只在 `cone_seen` 为 true 时更新冻结位置和回弹方向:
```cpp
// ★ 修复后:
if (g_cone_confirmed) {
g_cone_hold_ctr = 1;
g_cone_return_ctr = 0;
if (cone_seen) { // 只在锥桶实际可见时更新
g_cone_hold_src_row = cone_row;
g_cone_hold_src_col = cone_col;
double ms = mid_line[cone_row];
g_cone_return_dir = (cone_col < ms) ? -1.0 : 1.0;
}
}
```
同时修复 `src_row` 选择逻辑:
```cpp
// ★ 旧代码:
int src_row = g_cone_confirmed ? cone_row : g_cone_hold_src_row;
// 当 confirmed=true 但 cone_seen=false 时 → src_row = cone_row = -1 → 提前退出
// ★ 修复后:
int src_row = (g_cone_confirmed && cone_seen) ? cone_row : g_cone_hold_src_row;
// confirmed 但不可见时 → 使用冻结位置, 避让变形继续生效
```
修复后的时间轴:
```
时间轴: 锥桶可见 20帧 去抖衰减 18帧 hold 15帧 return 30帧
避让变形: ████████████████████ ████████████████████ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓
保持变形: (用冻结位置继续避让) (线性衰减→0)
回弹变形: ░░░░░░░░░░░░░░
```
## 8. 舵机与电机
**舵机:** 零改动。steering_update 读取 `mid_line[foresee/2]`,看到变形后的中线值,自然输出偏转方向。
**电机:** `motor_update()` 中存在锥桶减速代码(`cone_is_slow()``speed × cone_speed`),
但当前**已注释**,仅弯道减速生效。如需启用,取消 `motor_update()` 中对应行的注释即可。
---
## 9. 参数配置
所有锥桶参数均通过 `cfg_load_all()` 统一读取(`src/global.cpp`),debug 模式下每 30 帧热更新。
| 文件 | 类型 | 默认 | 含义 |
|---|---|---|---|
| `./cone_avoid_gain` | double | 0.3 | 归一化推离量 (0=不推, 1=推到赛道边缘) |
| `./cone_avoid_range` | int | 30 | 斜坡陡峭度 (越小越陡,foresee 行感受越强) |
| `./cone_speed` | double | 0.5 | 锥桶触发时速度倍率 (当前减速代码已注释) |
| `./cone_min_frames` | int | 1 | 确认帧数 (1=单帧即确认) |
| `./cone_margin` | int | 0 | 0=中心点过滤, 1=框边缘过滤 |
| `./cone_thresh` | double | 0.80 | 锥桶置信度阈值 (业务层, 不改 NMS) |
| `./cone_hold_frames` | int | 15 | 消失后保持变形帧数 |
| `./cone_return_gain` | double | 1.0 | 回弹力度 (1.0=等同避让力度) |
| `./cone_return_frames` | int | 30 | 回弹持续帧数 |
---
## 10. 代码位置
```
lib/image_cv.h:
└── extern mid_line_raw (原始中线快照声明)
src/image_cv.cpp:
└── std::vector<int> mid_line_raw (定义)
lib/global.h:
└── CfgCache 包含: cone_avoid_gain, cone_avoid_range, cone_speed,
cone_min_frames, cone_margin, cone_thresh,
cone_hold_frames, cone_return_gain, cone_return_frames
src/global.cpp:
└── cfg_load_all() 统一读取所有锥桶参数
src/camera.cpp:
├── CameraHandler(): image_main() 后执行 mid_line_raw = mid_line (快照)
├── 静态变量: g_cone_frames, g_cone_last_row/col, g_cone_confirmed,
│ g_cone_hold_ctr, g_cone_hold_src_row/col,
│ g_cone_return_ctr, g_cone_return_dir
├── cone_is_slow(): 判断是否处于避让/保持/回弹任一阶段
├── cone_detect_and_deform(): 方向判断基于 mid_line_raw, 变形修改 mid_line
├── motor_update(): cone_speed 减速 (已注释)
└── lcd_render(): 锥桶框橙色绘制
ctl.sh:
└── init_pins() 写入 cone_avoid_gain/range/speed/min_frames/margin/thresh/hold_frames
```
---
## 11. 方案对比
| | 旧方案(舵机推入) | 当前方案(中线变形) |
|---|---|---|
| 舵机代码 | 需改动 | 零改动 |
| deadband | 需绕过 | 自然生效 |
| steer_gain | 需单独 cone_gain | 共用 |
| 归还机制 | 需计时器+衰减 | hold 衰减 + return 回弹 |
| 平滑性 | 需 EMA | 斜坡自带平滑 |
| 参数数量 | 7 | 9 (含 hold/return) |
| 锥桶消失恢复 | 需手动衰减 | 自动: hold 衰减 → return 回弹 → 正常 |