模型推理移至管道末尾降舵机延迟60→16ms + 视觉Mat复用 + 端到端延迟计时 + OpenCV 4.13 + WASD遥控

This commit is contained in:
spdis
2026-06-29 16:43:41 +08:00
parent 40d5cb1604
commit 017a827b18
16 changed files with 1201 additions and 709 deletions
+99 -255
View File
@@ -1,232 +1,130 @@
# 激光雷达挡板避障设计
## 问题
## 概述
VL53L0X 单点 ToF 激光测距传感器,返回沿光束方向的一个距离值(mm,没有扇形扫描能力
激光测到近处有物体时,无法直接从距离值判断它是:
VL53L0X 单点 ToF 激光测距传感器,返回沿光束方向的距离值(mm)。
距离低于阈值时触发绕行:中线变形推向右侧,消失后保持衰减。
- A) **挡板/障碍物** — 横在赛道上的物体,需要绕行
- B) **赛道边墙** — 弯道处车头对准了侧墙,这是正常行驶状态
**解决思路:用视觉巡线的边界线(left_line / right_line)来区分 A 和 B。**
**当前实现:** 纯距离触发 + 预触发去抖,不检查视觉边界线。绕行方向固定朝右。
---
## 关键约束:挡板会导致丢线
## VL53L0X 驱动集成
挡板立在赛道上时,不仅触发激光近距读数,还会**遮挡摄像头视野中的赛道边界**,
导致从挡板所在位置开始边界线丢失(left_line / right_line = -1)。
驱动已编译(`src/vl53l0x.cpp` + `lib/vl53l0x/` C API),在 `CameraInit()` 中初始化:
```
Camera 俯视视角 (图像坐标):
row 0 (远) : ░░░░░░░ ← 被挡板挡住,丢线
row 15 : ░░░░░░░ ← 挡板上方,丢线
row 25 : ░░░░░░░ ← 挡板顶部附近,丢线
row 30 : ███████ ← ★ 挡板所在行,边界线丢失
row 35 : ■■■■■■■ ← 挡板下方,赛道可见,边界线有效
row 59 (近) : ■■■■■■■ ← 车前方,赛道清晰
CameraInit():
if g_cfg.lidar_enable:
g_lidar_ok = g_lidar_sensor.init() // open /dev/stmvl53l0x_ranging
g_lidar_sensor.startMeasure() // 启动首次测量
else:
g_lidar_ok = false // 禁用
cameraDeInit():
if g_lidar_ok → g_lidar_sensor.stop()
```
**这意味着:不能像锥桶检测那样"往更远处看边线是否还开着",因为挡板后面的边线必然丢失。**
正确的判定是检测 **"有效边界 → 丢线"的过渡位置是否与激光近距读数对应**:
- 紧贴着挡板下方(更近处):赛道应该可见,边界有效
- 挡板位置及上方(更远处):边界丢失
- 激光读数:短距离 → 同一位置有物理障碍物
三个条件同时成立 → 挡板确认。
硬件未连接时 `init()` 失败 → `g_lidar_ok = false``lidar_avoid_process()` 直接跳过,不影响正常行驶。
---
## 几何模型
## 流水线位置
`lidar_avoid_process()` 位于 CameraHandler Step 4,在视觉巡线之后、模型推理之前:
```
Camera + Laser
| (高度 H, 俯角 θ)
|╲
╲ laser beam
| ╲
ground ──────────────┴────███████──── 挡板 at distance d_mm
(挡板后方赛道被遮挡)
Step 3 image_main() → left_line[], right_line[], mid_line[]
Step 4 lidar_avoid_process() → 修改 mid_line[] (绕行变形) ★
Step 5 run_model_inference()
Step 6c cone_detect_and_deform() → 也修改 mid_line[] (叠加在 lidar 之上)
```
- 激光光束沿车体正前方(图像中轴线)
- 距离 d_mm 越小 → 物体越近 → 映射到图像中越靠下的行(row 大)
- 距离 d_mm 越大 → 物体越远 → 映射到图像中越靠上的行(row 小)
lidar 先于 cone 修改 mid_line,优先级更高。cone 的 clamp 确保不超出边界。
---
## 核心算法
### 第一步:读取激光距离
### 1. 读取激光距离(每 2 帧一次)
```
d_mm = vl53l0x.readRange().RangeMilliMeter
lidar_avoid_process():
if !g_lidar_ok || !g_cfg.lidar_enable → return
if ++lidar_skip < 2 → return // 每 2 帧一读 (~15Hz)
lidar_skip = 0
if d_mm >= LIDAR_THRESHOLD_MM:
return CLEAR // 远处无障碍,不做任何处理
data = g_lidar_sensor.readResult() // 非阻塞读取上次测量结果
d_mm = data.RangeMilliMeter
g_lidar_sensor.startMeasure() // 立即启动下次测量 (后台 ~20ms)
```
`LIDAR_THRESHOLD_MM` 是触发阈值。只有距离小于此值才进入判定。建议默认 ~300mm。
### 第二步:距离 → 图像行映射
### 2. 预触发去抖
```
// 线性模型:
// row = lt_h-1 (底行, 最近) 对应 D_NEAR
// row = 10 (最远有效行) 对应 D_FAR
// clamp 到 [10, lt_h-1]
if d_mm == 0: // 读失败, 跳过
skip
row = lt_h - 1 - (d_mm - D_NEAR) / (D_FAR - D_NEAR) * (lt_h - 11)
row = clamp(row, 10, lt_h - 1)
if d_mm >= g_cfg.lidar_pre: // 距离超出预触发窗口 (默认 800mm)
g_lidar_frames = max(0, frames-1) // 衰减但不归零
skip
// d_mm < lidar_pre → 进入触发判定
```
**标定值(需根据实际安装位置测量):**
`lidar_pre`(默认 800mm)是预触发窗口:距离进入此范围就开始攒帧,但要 < `lidar_thresh`(默认 300mm)才真正确认。
| 参数 | 含义 | 建议初值 |
|------|------|----------|
| `D_NEAR` | row = lt_h-1 对应的物理距离 | 50 mm |
| `D_FAR` | row = 10 对应的物理距离 | 1200 mm |
标定方法:在赛道前方 300mm、600mm、900mm 处各放一个挡板,记录图像中挡板出现的 row,线性拟合。
### 第三步:用边线判定障碍物(核心)
### 3. 距离 → 图像行映射
```
// 设 row = distance_to_row(d_mm),即激光测距对应的图像行
// ── 3a. 检查"挡板下方"(更近处):赛道应该仍可见 ──
near_valid = false
near_ref_row = -1
for r = min(row + NEAR_START, lt_h-1) down to max(row + NEAR_END, lt_h-1):
if left_line[r] != -1 && right_line[r] != -1:
near_valid = true
near_ref_row = r // 记录有效行,后续绕行时判断宽侧
break
// ── 3b. 检查"挡板位置及上方"(更远处):边界应该已丢失 ──
far_lost = false
for r = row down to max(row - FAR_SPAN, 10):
if left_line[r] == -1 || right_line[r] == -1:
far_lost = true
break
// ── 3c. 联合判定 ──
if near_valid && far_lost:
→ OBSTACLE_CONFIRMED
记录: g_lidar_obstacle_row = row
g_lidar_ref_row = near_ref_row // 用于判断绕行方向
else:
→ CLEAR
row = lt_h - 1 - (d_mm - lidar_near) × (lt_h - 11) / (lidar_far - lidar_near)
row = clamp(row, 10, lt_h - 1)
```
**判定原理(三种典型场景):**
| 参数 | 默认 | 含义 |
|------|------|------|
| `lidar_near` | 50 mm | row = lt_h-1(最近行)对应的距离 |
| `lidar_far` | 1200 mm | row = 10(最远有效行)对应的距离 |
### 4. 触发判定
```
场景 A: 挡板挡路 → 触发绕行
d_mm = 300mm → row = 35
row 36~39 (近处): ✓ 赛道可见
row 30~35 (挡板处): ✗ 丢线 (挡板遮挡)
→ near_valid=true, far_lost=true → ★ 触发
g_lidar_frames++
g_lidar_obstacle_row = row
场景 B: 正常直道,无障碍
d_mm = 1200mm → row = 10, 且 d_mm > THRESHOLD
→ 不进入判定,直接 CLEAR
场景 C: 弯道,激光打到边墙
d_mm = 200mm → row = 40
row 41~44 (近处): ✓ 赛道可见
row 37~40 (弯道处): ✓ 赛道也可能可见(边墙不一定导致丢线)
→ near_valid=true, far_lost=false → CLEAR
g_lidar_confirmed = (d_mm < lidar_thresh) && (g_lidar_frames >= lidar_min_frames)
```
### 第四步:去抖确认
**注意:** 不检查视觉边界线(near_valid / far_lost),纯距离触发。
配置中的 `lidar_near_start``lidar_near_end``lidar_far_span` 参数被加载但**未使用**(为设计预留)。
### 5. 确认 → 记录状态
```
if OBSTACLE_CONFIRMED:
g_lidar_frames++
else:
g_lidar_frames = max(0, g_lidar_frames - 1)
g_lidar_confirmed = (g_lidar_frames >= LIDAR_MIN_FRAMES)
if g_lidar_confirmed:
g_lidar_hold_ctr = 0
g_lidar_hold_src_row = g_lidar_obstacle_row
g_lidar_hold_dir = 1.0 // 固定朝右绕行
g_lidar_frames = 0 // 重置计数
```
**方向固定为右(+1.0**,不根据赛道左右空间动态判断。
---
## 绕行策略:中线变形
## 中线变形
挡板通常只挡赛道的一部分(偏左或偏右),通过判断挡板下方有效行中哪一侧空间更大,
将中线推向宽侧实现绕行。
### 判断绕行方向
`lidar_is_active()`(确认中或保持衰减中)时,对 row 10 ~ 障碍行施加变形:
```
// 在 near_ref_row(挡板下方最近的有效行)中判断左右空间
left_space = mid_line[near_ref_row] - left_line[near_ref_row]
right_space = right_line[near_ref_row] - mid_line[near_ref_row]
decay = (确认中) 1.0 : (1.0 - hold_ctr / hold_frames) // 保持期线性衰减
// 往宽侧推
dir = (left_space > right_space) ? +1.0 : -1.0
```
### 中线变形
对从 row=10 到挡板位置 row 的所有行施加变形,变形量从远到近线性斜坡上升:
```
// gain = lidar_avoid_gain (推离力度, 归一化)
// range = lidar_avoid_range (斜坡陡峭度, 行数)
// half_w = lt_w / 2
for r = 10 to g_lidar_obstacle_row:
t = clamp((r - 10) / range, 0.0, 1.0) // 斜率上升
push = t * gain * half_w * dir
mid_line[r] = clamp(mid_line[r] + push,
left_line[r] + 2.0,
right_line[r] - 2.0)
```
**变形示意图:**
```
row 10 ───────●──────────────────────────○ 赛道中心线
row 20 ────────●─────────────────────────○ (往右绕行)
row 30 ───────────●──────────────────────○
row 35 ──────────────●───────────────────○ ← 挡板位置
row 40 ────────────────████████████──────── ← 挡板 (丢线)
row 59 ─■────────●───■■■■■■■■■■■■■■───●───■ ← 车底 (近处可见)
left mid 方向盘往右打 right
```
### 速度联动
绕行时降速以保证安全:
```
if g_lidar_confirmed || g_lidar_hold_ctr > 0:
spd *= lidar_speed // 默认 0.4,即降至 40% 速度
```
### 消失后保持衰减
挡板不再被检测到后,变形不会立即消失,而是线性衰减(类似锥桶 hold 逻辑):
```
// 确认状态
if g_lidar_confirmed:
g_lidar_hold_ctr = 0
g_lidar_hold_src_row = g_lidar_obstacle_row
g_lidar_hold_dir = dir
// 衰减状态(挡板消失后)
else if g_lidar_hold_src_row > 0 && g_lidar_hold_ctr < lidar_hold_frames:
g_lidar_hold_ctr++
decay = 1.0 - (double)g_lidar_hold_ctr / lidar_hold_frames // 线性衰减到 0
for r = 10 to g_lidar_hold_src_row:
t = clamp((r - 10) / range, 0.0, 1.0)
push = t * gain * half_w * g_lidar_hold_dir * decay
mid_line[r] = clamp(mid_line[r] + push, left+2, right-2)
for row = 10 to g_lidar_hold_src_row:
t = clamp((row - 10) / lidar_avoid_range, 0.0, 1.0)
push = t × lidar_avoid_gain × half_w × dir × decay
mid_line[row] = clamp(mid_line[row] + push,
left_line[row] + 2.0,
right_line[row] - 2.0)
```
---
@@ -237,107 +135,53 @@ else if g_lidar_hold_src_row > 0 && g_lidar_hold_ctr < lidar_hold_frames:
┌────────────────────────┐
│ │
▼ │
┌──────┐ 确认 ┌──────┐ 消失 ┌──────────┐ 保持结束
│NORMAL│ ─────► │AVOID │ ─────► │HOLD_DECAY│ ────────► NORMAL
│ │ │ │ │ (衰减) │
└──────┘ └──────┘ └──────────┘
▲ │
└────────────────────────────────┘ 测距恢复正常
┌──────┐ 确认 ┌──────┐ 消失 ┌──────────┐ 保持结束
│NORMAL│ ─────► │AVOID │ ─────► │HOLD_DECAY│ ────────► NORMAL
│ │ │ │ │ (衰减) │
└──────┘ └──────┘ └──────────┘
▲ │
└────────────────────────────────┘ 测距恢复正常
NORMAL: 正常行驶,激光测距 > THRESHOLD
AVOID: 挡板确认 → 中线变形绕行 + 减速 × lidar_speed
NORMAL: 正常行驶,激光测距 > lidar_pre
AVOID: 挡板确认 → 中线变形绕行 (固定朝右)
HOLD_DECAY: 挡板消失 → 变形量线性衰减 (lidar_hold_frames 帧内归零)
衰减期间若再次检测到挡板 → 立即切回 AVOID
```
| 状态 | 电机 | 舵机 | 说明 |
|------|------|------|------|
| NORMAL | 正常速度 | 正常巡线 | 无变形 |
| AVOID | speed × lidar_speed | 中线偏转绕行 | 基于 left/right 空间判断方向 |
| HOLD_DECAY | speed × lidar_speed | 变形量线性衰减 | 确保车完全通过后再恢复 |
---
## 与现有流水线的集成
## 速度联动
`lidar_avoid_process()` 放在 cone_detect_and_deform 之前执行(两者都修改 mid_line
lidar 优先级更高):
```
// CameraHandler 中 Step 5:
bool zebra_block = zebra_process();
bool tl_block = traffic_light_process();
lidar_avoid_process(); // ★ 新增:直接修改 mid_line
cone_detect_and_deform();
steering_update();
motor_update(zebra_block, tl_block);
```
### 中线修改的优先级
lidar 和 cone 都会修改 `mid_line[]`。由于 lidar 绕行是结构性避障(避开整个挡板),
应**先执行 lidar 变形,再执行 cone 变形**。
cone 的 clamp 到 `[left+2, right-2]` 会确保不超出 lidar 变形后的安全区间。
### motor_update 速度联动
```cpp
// motor_update 中新增:
if (g_lidar_confirmed || g_lidar_hold_ctr > 0) {
spd *= g_cfg.lidar_speed;
}
```
不通过 `block` 参数刹车(绕行不需要停车),而是降速 + 变形。
---
## 复用现有 VL53L0X 驱动
已有驱动(当前被 `CMakeLists.txt` 排除编译):
```cpp
// lib/vl53l0x.h
VL53L0X sensor;
sensor.init(); // open /dev/stmvl53l0x_ranging
VL53L0X_RangingMeasurementData_t data;
sensor.readRange(data); // data.RangeMilliMeter
sensor.stop();
```
**集成时需做的事:**
1.`CMakeLists.txt` 的 EXCLUDE 列表中移除 `vl53l0x.cpp`
2.`CameraInit()` 中调用 `sensor.init()`(硬件未连接时优雅降级)
3.`cameraDeInit()` 中调用 `sensor.stop()`
`motor_update()` 中包含挡板减速代码(`lidar_is_active()``speed × lidar_speed`
但当前**已注释**。绕行仅靠中线变形,不减速。如需启用,取消注释即可。
---
## 配置参数
| 文件 | 类型 | 建议默认 | 含义 |
|------|------|----------|------|
| `./lidar_thresh` | int | 300 | 障碍判定距离阈值 (mm),低于此值触发判定 |
| 文件 | 类型 | 默认 | 含义 |
|------|------|------|------|
| `./lidar_enable` | int(0/1) | 1 | 激光避障总开关(0=禁用,init 也不调用) |
| `./lidar_thresh` | int | 300 | 障碍判定距离阈值 (mm),低于此值触发确认 |
| `./lidar_pre` | int | 800 | 预触发窗口 (mm),低于此值开始攒帧 |
| `./lidar_near` | int | 50 | row=lt_h-1 对应的物理距离 (mm) |
| `./lidar_far` | int | 1200 | row=10 对应的物理距离 (mm) |
| `./lidar_near_start` | int | 1 | near_valid 检测起始偏移 (row + N) |
| `./lidar_near_end` | int | 4 | near_valid 检测终止偏移 |
| `./lidar_far_span` | int | 6 | far_lost 检测跨度 (从 row 往上检查 N 行) |
| `./lidar_min_frames` | int | 3 | 连续确认帧数 |
| `./lidar_avoid_gain` | double | 0.4 | 绕行中线推离力度 (归一化) |
| `./lidar_avoid_range` | int | 30 | 绕行斜坡陡峭度 (行数) |
| `./lidar_speed` | double | 0.4 | 绕行时速度倍率 |
| `./lidar_speed` | double | 0.4 | 绕行时速度倍率 (当前未生效) |
| `./lidar_hold_frames` | int | 30 | 挡板消失后变形保持帧数 |
| `./lidar_enable` | int(0/1) | 1 | 激光避障总开关(0=禁用) |
| `./lidar_near_start` | int | 1 | (已加载但未使用,预留) |
| `./lidar_near_end` | int | 4 | (已加载但未使用,预留) |
| `./lidar_far_span` | int | 6 | (已加载但未使用,预留) |
---
## 边界情况 & 注意事项
1. **激光硬件未连接**`sensor.init()` 失败时`lidar_avoid_process()` 直接返回 false,不阻塞正常行驶
2. **弯道边墙误判** — 急弯处赛道边墙可能导致激光近距 + 边线丢失同时出现。依赖 `lidar_thresh` `lidar_min_frames` 调参抑制。误判时车会短暂绕向一侧,弯道通过后立即恢复
3. **左右空间相等**`left_space == right_space` 时默认 `dir = -1.0`(往右绕),可通过配置 `lidar_default_dir` 调整
4. **上坡/下坡**车辆俯仰变化会影响距离→行的映射精度。建议在平坦路段标定
5. **多传感器优先级** — lidar 先于 cone 修改 mid_line。zebra/tl 的 block 刹车优先级最高(红灯/斑马线停车不绕行)
6. **首次集成建议**先调通数据采集:打印 `d_mm`、对应 `row`、以及 `near_ref_row` 处的 `left/right/mid` 值,跑几圈确认标定参数无误后再开启绕行。
7. **挡板材质** — 深色挡板不会被 HSV-Otsu 判为赛道(白色 255),但仍会遮挡背景赛道导致丢线。判定逻辑依赖的是**丢线**而非**像素颜色**。
1. **激光硬件未连接**`init()` 失败时 `g_lidar_ok = false`,后续跳过,不阻塞
2. **弯道边墙误判** — 急弯处激光可能打到边墙产生近距读数。依赖 `lidar_thresh` + `lidar_min_frames` 去抖抑制
3. **方向固定右绕**当前不判断挡板在赛道左侧还是右侧,一律朝右推中线。对大多数场景够用,但若挡板在右侧则绕行方向不理想
4. **标定建议**在平坦路段用 debug 模式打印 `d_mm``row`,确认 `lidar_near` / `lidar_far` 映射准确
5. **VL53L0X 读取模式** — 非阻塞:先 `startMeasure()` 启动后台测量 (~20ms),下次调用时 `readResult()` 取结果。每 2 帧一读不阻塞主循环
6. **多传感器优先级**lidar 先于 cone 修改 mid_line。zebra/tl 的 block 刹车优先级最高(红灯/斑马线停车不绕行