水下 ROV 海底管道往返巡检:视觉感知 + EKF-SLAM + 学习式控制


1. 概述与核心能力

海底输油/输气管线是海洋工程里最需要常态巡检的基础设施之一:管线埋在浑浊水体里、 没有 GPS、能见度通常不到 3 m,而巡检本身有一个很硬的约束——必须走完再走回来。 A→B 单程跟踪做得好,不代表 B→A 能做;"管道在我的左边"这句话在两个方向上指的是 两个相反的横向方向,任何一处符号没转过来,ROV 就会在返程慢慢把管道推进自己的壳体。

仿真器说明(重要)

本模块不依赖任何 3D 引擎。海床、管道、相机与 ROV 动力学全部由仓库内自研的 解析式仿真器实现:相机是几何光线投射,管道求交是"线段 + 半径"的闭式解,推进器是 一阶滞后模型。它不是 HoloOcean / Gazebo / MuJoCo / CARLA 场景——这样做的目的是让 全链路在任意装了 ROS 2 的机器上逐位复现(CI 里也是这么跑的),代价是水动力与成像 比真实引擎简化,结论只在"相对比较"意义上成立。仓库里 water_vision_enhancement 同样是合成数据、未接 HoloOcean,有先例。

本模块构建一台三自由度 ROV 的完整巡检链路,四种海底场景(直管 / S 弯 / 横流 / 浑水) 下对比五种策略,核心能力有四块:

  1. 解析式海底仿真与传感器建模:

24 m × 16 m 海床平面,管道由 Catmull-Rom 样条生成稠密折线,相机光线与管道做 "线段 + 半径"的胶囊闭式求交,与圆柱应答器做二次方程求交。相机是 140° 广角 前视,128 × 96 分辨率,输出 RGB 与逐列常数深度(等价于一台 128 线单行深度 传感器)。ROV 侧建推进器一阶滞后、海流位移、浑水滑移三件事,全部纯 NumPy, 不需要 Gazebo / HoloOcean / CARLA,任何装了 ROS 2 的机器都能逐位复现。

  1. 颜色编码视觉感知与管道几何拟合:

海底布设 10 根颜色编码的声应答器做路标。检测端在归一化色度空间做最近邻 分类(对光照强度不敏感),管道用归一化色度距离单独判据分割,再用三条硬判据 (严格连续列段、每列 ≥2 像素、距离 ≤8 m)拒识散斑噪声。管道像素做二次曲线 拟合,输出"横向偏置 + 航向偏差"两个量直接喂控制器。

  1. EKF-SLAM 与占据栅格同步建图:

状态 = 位姿 + 全部已观测应答器,首次观测按标准公式做状态增广并保留与位姿的 相关性。往返任务里这一点是刚需:回程再次看到去程初始化过的应答器时,正相关性 就是位姿能被拉回起点的唯一信息来源,也就是回环闭合误差(loop_err)能收敛 而不是线性漂移的原因。栅格图由 SLAM 位姿增量写入,对数几率模型 + 管道胶囊 真值算出覆盖率与 IoU。

  1. 往返巡检控制与端到端对照:

级联控制 desired = dtheta + atan2(KP_LAT·dx_err, 1.4) + i_lat,大角度时原地 回转,横向积分项抗海流稳态偏差,折返用显式机动而不是让误差自然反号。并排训练 一个纯 NumPy 卷积网络做端到端(图像 → 速度指令),BC + 3 轮 DAgger,用来量化 "学习式策略在同一个任务上离经典流水线还有多远"。

graph TD subgraph World["解析式海底世界 World"] Pipe["Catmull-Rom 弯管
胶囊闭式求交"] LM["10 色声应答器
圆柱求交"] Cur["海流场
0.18 m/s 慢摆动"] end subgraph Sensor["广角前视相机 128x96 / FOV 140°"] RGB["RGB (rgb8)"] Depth["逐列常数深度 (32FC1)"] end subgraph Perception["感知 Perception"] Chroma["归一化色度最近邻
10 类应答器 ID"] Mask["管道橙分割
色度距离 < 0.13"] Reject["列段连续性 / 每列≥2px / ≤8m
散斑拒识"] Fit["二次曲线拟合
dx_err, dtheta"] end subgraph SLAM["EKF-SLAM 节点 rov_slam"] Pred["里程计预测
Q = 0.004²/0.012²"] Aug["状态增广
保留位姿相关性"] Gate["马氏距离门限 13.8
协方差膨胀 x1.8"] Grid["对数几率占据栅格"] end subgraph Control["巡检控制 rov_control"] Mission["任务阶段 go / return
按弧长进度翻转"] Cascade["级联律 + 横向积分
抗海流稳态偏差"] Turn["折返机动
背离管道定方向回转"] end Pipe --> RGB LM --> RGB Pipe --> Depth RGB --> Chroma RGB --> Mask Mask --> Reject --> Fit Chroma --> Aug Depth --> Aug Pred --> Aug --> Gate --> Grid Fit --> Cascade Mission --> Cascade Cascade --> Turn Turn -->|/rov/cmd_vel| Cur

2. 海底仿真世界与传感器建模

2.1 管道几何与胶囊求交

管道用 Catmull-Rom 样条从 5 个控制点插值成 160 段稠密折线(samples_per_seg=40)。 渲染时不能把管道当成"零宽度的线"——真实管线有 0.18 m 半径,侧面看过去有厚度, 而这层厚度恰好是横向偏置估计能成立的原因。

射线与"线段 + 半径"胶囊的最近命中距离有闭式解。设射线 o + t·d、管道段 p1 + s·ê, 把 |o + t·d − (p1 + s·ê)|² 对 t、s 联合最小化,正交条件给出 t = D + B·s (B = ê·d,D = m·d,m = p1 − o),代入后 s* = (E + B·D)/(1 − B²), s 夹到 [0, L] 即自动处理端点。整段只做一次向量化点积,128 条射线对 160 段 共 2 万个求交在毫秒级完成,这是整条流水线能跑满 10 Hz 的前提。

应答器是半径 0.10 m、高 0.90 m 的立柱,射线与圆柱在俯视平面上的求交就是一个 二次方程取近根。高度用于可见性判定:0.90 m 这个数字是被调试出来的——早期 设定 0.65 m 时,相机平面在 0.45 m,只比应答器顶端低 0.2 m,远距离上立柱几乎 退化成一条线,8 m 外基本检测不到,SLAM 在直管中段就丢了观测。

2.2 三自由度水下运动学

# kinematics.py(节选)
s = s + (v_cmd - s) * dt / SURGE_TAU          # 推进器一阶滞后,SURGE_TAU = 0.6 s
pose += R(th) @ (s * dt, 0) + current * dt    # 海流是纯位移,不进里程计
th   += w * dt
odom += R(th) @ (s * dt, 0)                   # 里程计只积分推力,看不到海流

关键的一点写在最后一行:海流漂移不进入里程计。真实 ROV 若无 DVL,惯导只能 积分推进器指令,横流造成的位移完全不可观测——这正是 current 场景里 SLAM 必须 靠视觉观测把位姿拉回来的物理来源。浑水场景额外把滑移系数放大到 1.25,模拟 悬浮泥沙造成的推进效率下降。

2.3 广角前视相机的逐列投射

相机 128 × 96、FOV 140°(焦距 ≈ 23 像素)。140° 是被任务逼出来的:横向偏置 0.9 m 跟管时,管道在近距离会跑到视野边缘,90° 视场下当 ROV 稍微偏出就会丢管道。

渲染时取 128 个列方向各投一条射线,得到每列的命中距离后逐列填充一个常数深度 ——这就是"单行深度传感器"的等价物。选择逐列常数而不是真实深度图,是为了让 几何感知与 CNN 感知站到同一个信息量上:CNN 拿到的深度通道也是这块逐列信息, 两者的差异只剩"谁更会从颜色里抠结构",而不是"谁多拿了一份深度"。

浑水用线性雾化模拟:c_obs = (1 − α)·c_obj + α·c_water,α = 0.55,并按距离 加权。这是简化的前向散射模型(没有体积散射积分),因此下文的结论对 CNN 感知 是偏乐观的,已在"已知局限"里注明。


3. 感知:颜色编码应答器与管道分割

3.1 归一化色度空间

RGB 绝对值随光照衰减剧烈变化,水下尤其如此。所有分类都在归一化色度 c = rgb / Σrgb 上做,把"亮度"这个维度彻底消掉。10 色调色板按三条约束挑选:

约束 阈值 目的
两两间距 ≥ 0.19 分类阈值 0.10,留一倍噪声余量
离水色(蓝绿) ≥ 0.20 水背景不能被误分类成应答器
离管道橙 ≥ 0.19 管道与应答器互不混淆

这三条不是凑出来的。最初调色板里有"灰蓝"和"青"两个颜色,与水体背景的色度 距离只有 0.089 和 0.137,结果整幅水背景被判成应答器,真正的应答器淹没在大块 背景里检测不出来——现象是 SLAM 在 current / turbid 场景下 ATE 从 0.3 m 暴涨到 2~4 m,而清水场景完全正常,极难定位。换成 10 色、并把水体自身色度也作为 一个"必须排除的类"后问题消失。白色同样被淘汰过:饱和度 0.04 过不了 SAT_TH = 0.34 的门限,最后换成春绿 [0.20, 0.85, 0.50]。

3.2 管道掩膜:为什么不能用"橙通道占优"

第一版判断管道用的是"R 通道显著大于 G、B"。这在清水里没问题,但红色应答器 [0.85, 0.15, 0.10] 的 R 通道占比 0.77,比管道橙的 0.65 还高——整根红色应答器 被当成管道,ROV 从起点就开始绕着它打转,pipe_slam 在直管场景 12 次里只成功 1 次且耗时 220 s(打满时间上限)。

改成归一化色度上的欧氏距离判据:‖c − c_pipe‖ < 0.13,其中 c_pipe = [0.647, 0.317, 0.033]。红色 [0.773, 0.136, 0.091] 到管道橙的距离 是 0.30,安全地落在门限外。

3.3 拒识比分类更难

真正让 SLAM 崩掉的不是分类错误,而是虚假观测:浑水场景下渲染噪声会形成 几十像素的随机色块,检测端把它当成一根 11.6 m 外的应答器,EKF 用一条这样的 观测就把位姿初始化到 (16.45, 20.18)——离真值 6 m 开外,之后再也拉不回来。

三条硬判据把这类观测全部拒掉:

  1. 严格连续列段:同一根立柱在相邻列上必须连续出现,断一列就切断;
  2. 每列 ≥2 像素:散斑在单列上通常只有 1 个像素;
  3. 观测距离 ≤ 8 m:超过 8 m 的检测即使是真的也不可信(信噪比太低)。

第 2 条最有效。实测加了这三条之后,pipe_slam 在直管 / 弯管两个清水场景的 ATE 从 0.9~1.4 m 降到 0.28~0.63 m,current 场景从"完全失效"变成"能跑完但 横向偏差偏大"。

3.4 管道拟合与 CNN 感知支路

管道像素按列取中位行,做一次二次曲线拟合 u(row) = a + b·v + c·v²,从曲线在 图像底部与中部的斜率/截距反解出横向偏置 dx_err 与航向偏差 dtheta。用二次 而不是直线,是因为 140° 广角下管道在图像里明显弯曲,直线拟合在近距离会系统性 低估横向偏置。

几何方法在清水里很准(直管横向误差 0.11 m),浑水里退化但不失效:雾化把管道 颜色按 0.55 的比例混进水色,色度判据给出的是断续的列段,横向误差涨到 2.0 m、 成功率从 3/3 掉到 2/3。我们同时训练了一条 CNN 感知支路(percep.npz,2 通道 输入:灰度 + 逐列深度)直接回归 (dx_err, dtheta),想看看"学习式感知能不能 把这块补回来"——结果是 0/12,原因见 §7.3。


4. EKF-SLAM 与占据栅格建图

状态向量 μ = [x, y, θ, m₁x, m₁y, …, m_kx, m_ky],协方差 Σ 随应答器数量 增广。预测步吃里程计增量(不是绝对位姿),更新步吃"距离 + 方位"观测:

  • 测距噪声 σ_r = 0.05 + 0.02·r(米),方位噪声 σ_b = 20 mrad;
  • 数据关联用马氏距离门限 13.8(2 自由度卡方分布的 99.9% 分位);
  • 被连续拒绝的观测不丢弃而是协方差膨胀 ×1.8("自我怀疑"机制):如果同一个 应答器连续 3 次被拒,说明更可能是自己的位姿错了而不是观测错了,此时膨胀协方差 让滤波器愿意被观测拉走。这一条在折返瞬间尤其重要——那时位姿误差最大,观测最 不可靠,纯硬门限会把所有观测都拒掉,滤波器彻底失去约束。

过程噪声 Q = diag(0.004², 0.004², 0.012²) 是权衡结果:ROV 的里程计误差来自 推进器滞后与未知海流,是系统性漂移而非白噪声。Q 给小了滤波器不信任观测、越漂越远; Q 给大了滤波器把每个含噪观测当真、末端定位同样劣化。按测距/方位噪声标定后, 这组取值在两个方向上都最稳。

栅格 240 × 160(0.1 m/格),对数几率模型 L ← L + hit/miss,夹在 [−4, 4]。 真值图用的是管道胶囊而不是中心线——早期版本 pipeline_mask 只标中心线, 导致"管道覆盖率"这个指标永远偏低(管道本体有 0.36 m 宽,中心线只占其中一列), 看起来像建图漏检,实际上是评测写错了。


5. 往返巡检控制

5.1 级联律

desired = wrap(dtheta + atan2(KP_LAT * dx_err, 1.4))   # 航向 + 横向修正
w = KP_TH * desired                                     # KP_TH = 1.9
v = MAX_V * clip(1 - 0.55*|dx_err| - 0.50*|desired|, 0.25, 1.0)

atan2(KP_LAT·dx_err, 1.4) 这一项是"横向误差 → 期望航向"的映射,1.4 是收敛距离 尺度:dx_err = 1.4 m 时正好偏 45°。用 atan2 而不是线性比例,是为了让大偏差 时转角有上界(±90°),不会因为误差突然跳变而给出荒谬的转向指令。

大角度必须原地回转:|desired| > SPIN_ENTER(0.70) 时把 v 压到 0, SPIN_EXIT(0.45) 才恢复前进——两个阈值不同构成迟滞,避免在阈值附近抖动。 这条在折返时是救命的:边前进边大角度转,ROV 会画一个大弧扫到管壁上。

5.2 任务系 vs 车体系的符号

这是整个作业里最贵的 bug。控制器需要的是任务系下的横向误差(相对巡检方向), 而检测器给出的是车体系下的(管道在车体左边还是右边)。单向跟踪时两者恰好 一致,往返任务里返程符号必须翻转:

dx_err = mdir * (FOLLOW_OFFSET - lat)     # mdir = +1 去程 / -1 返程
dtheta = wrap(atan2(mdir*ty, mdir*tx) - th)

第一版直接用 lat(管道在车体系的横坐标)当误差,结果是:去程 12/12 完美, 返程 0/12 全部失败——ROV 一路把管道往右推,横向距离从 0.88 m 掉到 0.38 m (管道半径 0.18 + ROV 半径 0.25 = 0.43 m 就已接触),最后压进管壁。修改后 oracle 12/12,直管横向误差 0.03 m。

同样的问题也污染过评测指标:lat_rmse 最初算的是 abs(lat),那是"到管道的 距离",恒等于 0.9 左右(因为偏置巡检本来就在 0.9 m 处),根本反映不了跟踪 好坏。改成 abs(lat − FOLLOW_OFFSET) 之后直管场景报出 0.03 m,指标才恢复意义。

5.3 横向积分项是抗流的唯一手段

横流 0.18 m/s、推力上限 0.55 m/s,稳态横向偏差稳定停在 0.3 m 左右。纯比例 控制消不掉它,因为偏差的来源(流速)不在误差信号里——误差收敛到 0 时 ROV 仍在被推走,只是推力把这一位移压到了一个平衡点。

加一路积分(KI_LAT = 0.05,I_LAT_MAX = 0.5)后稳态偏差压到 0.1 m 以内:

# 回转/大偏差期间 step() 在积分之前就 return,积分自动冻结
i_lat = clip(i_lat + KI_LAT * DT * dx_err, -I_LAT_MAX, I_LAT_MAX)
desired = wrap(dtheta + atan2(KP_LAT*dx_err, 1.4) + i_lat)

两个细节:积分在回转期间冻结,否则折返瞬间那个"看起来巨大"的横向偏差会把 积分项直接冲到限幅,然后一直带着一个 0.5 rad 的偏置飞到返程终点;积分必须先 限幅再叠加,否则一次持续 3 s 的大偏差就能让积分项吃掉全部控制余量。抗流是否 真的起作用,看 current 场景的横向误差与清水场景的差距就知道了——二者差距被 压到 0.1 m 量级以内,说明稳态偏差确实是被积分项而不是被某个巧合抵消的。

5.4 折返是一次显式机动

管道走到端点 B 时,视野里只剩端头那一小段,拟合出来的航向偏差是垃圾值。如果 仅靠"任务方向反号",控制器会基于这个垃圾值让 ROV 原地打转,甚至蹭到管口。

做法是在相位翻转的瞬间调 begin_turnaround(dir):按"管道在鼻子哪一侧"决定 旋转方向(往背离管道的那边转,保证肚子里有空间),一直转到航向误差 < SPIN_EXIT 才交回正常级联控制。return 结束回到 A 时同样处理,只是因为到 A 就结束了、 不需要再转回来。

阶段推进不用"到点距离"而用弧长进度:偏置巡检时 ROV 永远到不了端点本身 (端点就在管道正上方,而 ROV 在 0.9 m 外),用距离判据永不触发。现在 s ≥ length − 0.8 翻到 return,s ≤ 0.8 判定完成。


6. 端到端策略与 DAgger

网络是纯 NumPy 实现的 4 层卷积 + 全连接(im2col 前向,手写反向),输入 5 通道:

通道 内容
0–2 连续 3 帧灰度(128 × 96,/255)
3 逐列常数深度(归一化)
4 当前目标方位(HUD 式,由里程计位姿与任务端点算出)

输出 2 维:(v/MAX_V, ω/MAX_W),都在 [-1, 1]。

训练流程:专家数据 → 行为克隆(40 epoch)→ 3 轮 DAgger(β = 0.7 / 0.35 / 0) → 保存 e2e.npz。感知网络(2 通道:灰度 + 深度)走同一条数据流水线,DAgger 产生的"学生视角帧"也会同步微调它——学生看到的分布才是部署分布。

第 4 通道(目标方位)的动机来自地面那次端到端实验:走廊里同一幅画面既可能对应 "直行"也可能对应"左转",正确动作取决于目标在哪儿,而目标不在图像里,结果 行为克隆学到均值后撞墙。给一路由里程计算出的目标方位(不需要地图、不需要 SLAM) 之后才起来。

但这次消融没复现那个结论:e2e_nogoal 用同一份权重把第 4 通道置零, 结果同样是 9/12(直管横向误差 0.08 m vs 0.04 m,弯管 0.16 m vs 0.21 m, 互有胜负)。原因很清楚——管道本身就是一条连续的引导线,紧跟它就能走完 全程,唯一需要判断方向的是端点折返,而端头几何在图像里是可见的。地面那次 "不可辨识"是因为走廊没有任何连续引导物。所以这路通道在这里是锦上添花而不是 必要条件;我们保留它,是因为它把"任务级目标"显式喂进策略,换成多航点巡检时 会立刻变成刚需。


7. 实验与结果

7.1 协议

  • 场景 4 个:straight(直管)、curve(S 弯)、current(横流 0.18 m/s)、 turbid(雾化 α = 0.55);
  • 种子 3 个(0/1/2),应答器布点与管道微扰随种子变化;
  • 策略 4 个进主对比:oracle(真值位姿 + 真值横向误差,上界)、pipe_slam (几何检测 + EKF-SLAM)、e2e、e2e_nogoal(把目标方位通道置零做消融); 另有一条 pipe_cnn(CNN 回归 (dx, dtheta) 替代几何检测)不进主对比, 结果与归因单列在 §7.3;
  • 训练数据只来自清水场景(straight + curve),current / turbid 只做测试——检验泛化,不是背布局。

指标:成功率(走完 A→B→A 且不碰撞)、横向误差 RMSE(相对巡检偏置线)、 ATE、回环闭合误差、管道覆盖率、建图 IoU。

策略 场景 成功率 横向误差 (m) ATE (m) 回环误差 (m) 管道覆盖 建图 IoU 用时 (s)
Oracle(真值) 直管 3/3 0.03 — — — — 66
Oracle(真值) S 弯 3/3 0.11 — — — — 99
Oracle(真值) 横流 3/3 0.35 — — — — 80
Oracle(真值) 浑水 3/3 0.11 — — — — 99
几何感知 + EKF-SLAM 直管 3/3 0.11 0.29 0.04 0.12 0.19 69
几何感知 + EKF-SLAM S 弯 3/3 0.27 0.50 0.05 0.24 0.24 96
几何感知 + EKF-SLAM 横流 1/3 0.47 2.76 3.43 0.09 0.05 59
几何感知 + EKF-SLAM 浑水 2/3 0.37 1.87 0.05 0.13 0.05 118
端到端 CNN 直管 3/3 0.04 — — — — 79
端到端 CNN S 弯 3/3 0.21 — — — — 109
端到端 CNN 横流 3/3 0.28 — — — — 85
端到端 CNN 浑水 0/3 — — — — — 88
端到端(目标通道置零) 直管 3/3 0.08 — — — — 79
端到端(目标通道置零) S 弯 3/3 0.16 — — — — 111
端到端(目标通道置零) 横流 3/3 0.30 — — — — 85
端到端(目标通道置零) 浑水 0/3 — — — — — 44

(误差列只统计成功回合;— 表示该策略不产生这个量,例如端到端不起地图。)

消融这一行不能当作"目标通道有用"的证据:e2e 与 e2e_nogoal 都是 9/12, 逐种子看直管 0.040 / 0.077 m、S 弯 0.213 / 0.157 m、清水均值 0.127 / 0.117 m, 方向互相矛盾,属随机波动。这一行的实际结论是反过来的:本任务里端到端策略 并不依赖显式目标方位通道,去掉它不掉点。不要把"含消融"读成"消融有效"。

合计:oracle 12/12、几何流水线 9/12、端到端 9/12、端到端置零 9/12。

三条结论:

  1. 经典流水线在清水里已经接近上界:直管横向误差 0.11 m(oracle 0.03 m)、 ATE 0.29 m、回环误差 0.04 m;S 弯则分别是 0.27 m / 0.50 m / 0.05 m—— 弯道的近端拟合偏差让横向误差翻了一倍多,所以"0.11 m"只对直管成立。 即便在直管,往返 34 m 之后位姿还能对回起点 4 cm,靠的就是去程初始化过的 应答器在返程被再次观测。
  2. 两类策略的失败位置完全不同:流水线在横流崩(1/3,ATE 2.76 m)、 在浑水掉到 2/3,都是定位/建图的问题;端到端在清水/弯管/横流 9/9 全过, 唯独浑水 0/3,是图像分布的问题。同样是 9/12,失败原因不在一个维度上。
  3. 目标方位消融没有显著差异(9/12 vs 9/12,直管横向误差 0.04 vs 0.08 m)。 这和地面那次端到端实验的结论相反,原因见 §6 末尾。

四场景 × 四策略对比:成功率 / 横向跟踪误差 / SLAM 定位误差 / 管道建图覆盖

往返回合轨迹对比(直管,seed 0):oracle、几何流水线、端到端

训练曲线:感知网络 / 行为克隆 / 三轮 DAgger

S 弯巡检演示(几何流水线,含 EKF-SLAM 栅格叠加)

浑水巡检演示(同一套几何流水线)

(对比图中的误差取全部回合的平均,含失败回合,因此 (b) 里浑水一组的 流水线柱子是 0.80 m;上面表格只统计成功回合,两处口径不同。)

7.2 失败归因

横流场景(流水线 1/3):ATE 冲到 2.76 m、回环误差 3.43 m,两个失败回合里 有一个直接把 ROV 带到 11 m 开外。这条链路的信息全部来自视觉观测,而横流下 ROV 相对应答器的位移持续变化,观测更新率跟不上漂移,滤波器被"协方差膨胀 ×1.8" 的自我怀疑机制带着一起漂——膨胀本意是让滤波器在折返时能接受观测,副作用是 在长时间无观测的段里把先验也放软了。更关键的是阶段推进用的是 SLAM 位姿: 位姿漂 2 m 之后,"到达 B 端"这个判定本身就错了,折返机动在错误的位置触发, 后面再怎么控制都没用。这是本任务里位姿误差与控制决策耦合最紧的地方。

浑水场景(流水线 2/3,端到端 0/3):流水线靠几何检测仍然完成了 2/3, 因为线性雾化保留了颜色比例关系,色度判据只是变糙(横向误差 0.37 m);失败的 那一次是感知连续丢失超过 3 s 触发了 A* 兜底,而在建得不好的栅格上规划出来的 路径把它带偏了。端到端则是一次都没成:训练集只有清水,浑水的对比度分布完全 不同,网络输出的速度指令在 32~150 s 内累计偏到压管为止。学习式策略的失败 是分布问题,不是算法问题——这一点在"训练/测试场景刻意不重叠"的设计下被 放大了,也正是这个设计想要暴露的东西。

直管/弯管(流水线 6/6):横向误差 0.11 / 0.27 m,都在 LAT_OK = 0.45 的 合格线内;弯管会更差一些,因为偏置 0.9 m 的巡检线在弯道内侧会被管道自身"挤" 短,二次曲线拟合在近端有系统性偏差。

7.3 负面结果:用 CNN 回归几何量替代几何检测

我们试过把感知整条换掉:训练一个 2 通道(灰度 + 逐列深度)的卷积网络直接回归 (dx_err, dtheta),输出接同一套级联控制。两版都失败,但失败原因不同, 值得记下来。

第一版:均值坍缩。 用纯专家数据训练(BC 留出 MSE 1.1e-3),闭环 12/12 全部超时且位移为零——220 s 打满,终点距起点 0.98 m,即根本没动。原因是 专家轨迹本身的横向误差几乎恒为 0(直管 0.034 m、弯管 0.114 m),MSE 意义下 最优解就是"恒输出 0"。把 ROV 直接摆在偏 1.77 m 的位置上测试,网络仍报 -0.01: 它不是没学会,是没学过"偏了"是什么样子。行为克隆的经典陷阱,在"回归一个 几乎恒为 0 的量"时尤其致命。

第二版:能回归,但闭环抖。 采集时给起点加 ±0.55 / 0.35 m 的横向偏置 (两侧幅值刻意不对称:偏置线只有 0.9 m,朝管道那侧超过 0.40 m 起点就在接触区里), 标签覆盖到 ±0.5 m。网络确实学会了:偏 0.25 m 报 0.25 m,偏 0.45 m 报 0.53 m。 但闭环仍是 0/12,失败模式变成4~6 s 内撞上应答器:逐帧记录显示回归量帧间 抖动达 ±0.3 m,|desired| > 0.70 的回转判据被抖动反复触发,ROV 进入"转一下、 停一下"的状态,横向漂移 1.0 m 后撞上路侧 1.8 m 外的应答器。

结论不是"网络太小"(BC 留出误差已经在 1e-3 量级),而是几何量回归对帧间 一致性要求极高,而逐帧独立回归天然给不出这种一致性;解析几何方法(列段拟合) 则天然是连续的。因此主对比保留几何检测,CNN 感知作为对照与后续改进的入口 保留在代码里(make_policy('pipe_cnn')、use_cnn:=true)。

顺带一个代价:这批起始扰动样本会污染端到端策略的数据分布(学生被大量"救偏" 样本带偏,实测 9/12 → 0/12),所以 collect_demo 默认不扰动,需要时显式传 jitter= 复现本节的实验。


8. 一键运行

# 离线流水线(不需要 ROS)
cd src/water/rov_pipeline_follow
python -m rov_pipeline_follow.main              # 采集→训练→评测→出图,约 25 min
python -m rov_pipeline_follow.main --quick      # 快速版,约 3 min
python -m rov_pipeline_follow.main --stage eval # 只评测

# ROS 2 闭环(Humble)
colcon build --packages-select rov_pipeline_follow
source install/setup.bash
ros2 launch rov_pipeline_follow main.launch.py scene:=curve seed:=0
ros2 launch rov_pipeline_follow main.launch.py scene:=turbid use_cnn:=true
ros2 launch rov_pipeline_follow teleop.launch.py        # 键盘遥控
ros2 launch rov_pipeline_follow e2e.launch.py           # 端到端策略

# 测试
python -m pytest test/ -q      # 无 rclpy:55 passed;装了 rclpy:49 passed, 6 skipped

test/test_nodes_ros.py 的 6 个用例走的是 fake_ros 替身(在 fake_ros.install() 检测到真实 rclpy 时会主动 pytest.skip),所以在 Ubuntu + Humble 上跑出来是 49 passed, 6 skipped,不是 55 passed;那 6 个用例要验证的端到端行为请用 ros2 launch 走 §8 的冒烟,CI 里两段都覆盖。

CI(.github/workflows/ros2_rov_pipeline_follow.yml)在 ros:humble 容器里跑: 离线测试 → colcon build → install 布局硬断言 → 四场景各 180 s 真 launch 冒烟 → 权重加载冒烟 → 离线闭环回归(oracle 直管 + pipe_slam 弯管必须走完)。


9. 总结

这个任务真正的难点不在"怎么跟踪管道",而在往返这两个字带来的三个连锁问题: 任务系与车体系的符号、折返瞬间的观测失效、以及海流造成的稳态偏差。三个都不是 加一层网络能解决的,必须回到几何与控制里去修。

同时它也划出了两条技术路线的边界,而且边界的位置不一样:

  • 解析几何 + EKF-SLAM 的边界在定位——清水直管横向误差 0.11 m(S 弯 0.27 m), 往返 34 m 后位姿还能对回 4 cm;横流下位姿漂 2.8 m,因为阶段推进用的是 SLAM 位姿, 位姿一错,折返时机就错,后面全崩;
  • 端到端的边界在图像分布——清水/弯管/横流 9/9 全过,浑水 0/3,训练时 没见过雾化就真的不会。

两者拿到同样的分数(各 9/12),但失败的维度完全不同,这比"谁高谁低"更有 信息量:要提升鲁棒性,流水线该补的是 DVL/声学定位,端到端该补的是域随机化。