水下管道智能巡检这类项目视觉部分往往是最先被寄予厚望、也最容易在现场掉链子的一环。实验室水池里跑得好好的检测模型一下真实水体就开始漏检明明肉眼能看见的裂缝算法给出的框却飘到管道外面去了。我在做这套水下管道智能巡检系统的视觉模块时前前后后返工了三次最后一次几乎把预处理链路推翻重写。这篇就把视觉这条线上踩过的坑、做过的取舍、以及那些文档里不会写的调参顺序摊开讲一遍给同样在做水中机器人工程训练、尤其是负责图像这一块的朋友省点时间。内容覆盖水下图像退化建模、增强链路的搭建顺序、管道本体的分割与轴线拟合、缺陷检测通道、像素到物理量的换算标定以及数据集构建中最容易被低估的工作量。不要求你已经有深度学习基础但至少要能看懂 Python 和 OpenCV 的基本调用。1. 水下视觉为什么不等于把陆地上的方案沉下去1.1 光在水里走一趟图像就已经不是原来那张图了很多人第一次拿到水下相机的图第一反应是怎么这么绿/这么蓝然后就想着加个饱和度或者拉一下对比度完事。这个思路在清澈水池里确实能糊弄过去但只要水体稍微浑一点问题就会暴露得非常彻底。水对光的吸收是有强烈波长选择性的。清水里红光衰减系数大概在 0.30.4 /m 量级蓝绿光只有 0.020.05 /m差了将近一个数量级。这意味着只要拍摄距离超过一米红色通道的信息基本就已经所剩无几图像整体会向青蓝色偏移。更麻烦的是散射悬浮颗粒把光线乱反射回来形成一层均匀的雾这叫后向散射它直接把整幅图的对比度压下去而前向散射会让边缘变糊相当于给镜头加了一个随距离变化的高斯模糊。这两个效应叠加的结果是同一段管道在 0.5 米处和 2 米处拍出来颜色分布、边缘锐度、甚至目标的表观尺寸都会发生非线性变化。我最初的检测模型就是栽在这上面——训练集里绝大多数样本拍摄距离在 0.8 米以内模型实际上学到的是近距离管道纹理一旦机器人拉开距离召回率直接掉到六成以下。所以第一件想清楚的事是水下图像退化不是噪声问题是成像模型问题。你要处理的是 J(x) I(x)·t(x) A·(1−t(x)) 这个物理过程其中 t(x) 是随距离指数衰减的透射率A 是水体背景光。理解这一点之后你就不会指望单纯调个 gamma 就能解决问题而会去考虑分通道补偿和透射率估计。1.2 巡检任务到底要视觉回答哪几个问题工程训练项目最容易犯的毛病是什么功能都想加最后每个都做不深。视觉模块在整条链路上真正需要交付的输出其实就那么几项我在动手写第一行代码之前先把它们列清楚了输出项用途精度要求更新频率管道区域 mask判断机器人是否偏离管路像素级允许 5% 误差10 Hz管道中心线偏移量横向纠偏控制的输入±2 cm10 Hz缺陷目标框裂缝/附着物/法兰记录与报警位置误差 5 cm5 Hz 即可前方障碍物距离避障减速±10 cm15 Hz关键帧图像事后人工复核原图事件触发这张表定下来之后整篇文章后面的所有技术选型都围绕它展开。比如缺陷目标框 5 Hz 就够直接决定了我不需要为了帧率去牺牲输入分辨率而中心线偏移 ±2 cm反过来要求我必须做严格的像素-物理换算标定不能拍脑袋给个系数。提示如果你的项目还没定这张表先别急着训模型。输出的精度要求和更新频率会反向决定网络大小、输入尺寸、部署平台甚至决定你要不要上双目。这些都是改起来代价很高的决策。2. 预处理链路先把看得清做到及格线2.1 红通道补偿与对比度增强顺序不能反水下增强的算法一大堆从灰度世界白平衡、伽马校正到暗通道先验去雾、Retinex 系列再到近两年各种基于生成模型的方案。我在选型时挨个试过最后落地的是一条很朴素的链路因为它足够快、足够可解释、参数的物理含义清楚出问题能定位。链路顺序是红通道补偿 → 灰度世界白平衡 → CLAHE 局部对比度增强 → 轻度锐化。这个顺序是有讲究的。红通道补偿必须放最前面因为它是一个基于通道间相关性的线性修正你如果先做了 CLAHE局部对比度被拉伸过通道间的线性关系就被破坏了补偿系数会估得很离谱。红通道补偿用的公式很经典import cv2 import numpy as np def red_channel_compensate(img, alpha1.0): # img: BGR uint8 b, g, r cv2.split(img.astype(np.float32)) # 用绿通道的均值作为参照补偿红通道 r_new r alpha * (g.mean() - r.mean()) r_new np.clip(r_new, 0, 255) return cv2.merge([b, g, r_new]).astype(np.uint8)alpha这个系数我实测下来在 0.81.2 之间比较合适。清澈水池取 0.8 就够浑水取 1.2 甚至 1.4。它本质上是在补多少红和引入多少假色之间做权衡补过头了管道表面会泛出不自然的品红色反而干扰后续分割。然后是灰度世界白平衡假设整幅图的平均色应该是灰的用它去估一个通道增益。这个假设在水下其实并不总是成立——如果画面里大半是蓝绿色的水平均色偏蓝是物理事实强行拉灰反而会让水体区域过曝。所以我加了个保护只在管道 mask 覆盖率超过 30% 的帧上启用完整白平衡否则只做半强度补偿。CLAHE 是这条链路里最见效也最危险的一步。参数上clipLimit我最终定在 2.5tileGridSize用 8×8。这两个值的确定过程在下一节细说。2.2 增强过头会反噬检测我的参数调试顺序这里必须讲一个很多人忽视的问题增强效果的目视评价和检测效果的评价方向经常是相反的。我最开始调 CLAHE 的clipLimit从 1.0 一路加到 6.0用肉眼看6.0 的时候管道表面的锈斑、划痕、附着物纹理全都跳出来了特别清晰当时觉得这下稳了。结果拿去做分割评测mIoU 反而比 2.0 的时候低了 4 个点。原因很直接clipLimit太大的时候均匀的水体背景也被拉出了大量噪声纹理分割网络把这些纹理误判成管道边缘mask 边界开始毛刺化进而把后面拟合出来的中心线带偏。所以我的调试顺序是这样的建议你照抄先固定增强参数只调网络。用一组保守参数clipLimit2.0训一版基线记住它的 mIoU 和中心线偏移的 RMS。再做增强参数的网格扫描但评测指标不看 mIoU看下游任务指标——也就是中心线偏移的 RMS 和缺陷框的 IoU。这才是真正关心的东西。最后做鲁棒性检查把同一段视频分别用清水、微浑、浑水三档水体拍摄看参数在哪一档开始崩。扫描结果里有个很有意思的现象clipLimit在 2.03.5 之间时中心线 RMS 基本持平在 3.5 以上急剧变差但缺陷框 IoU 是在 2.5 左右达到峰值再往上因为噪声反而下降。两个指标的最优点不重合最后取了 2.5 这个折中值。你看如果不分开测光看 mIoU 你根本发现不了这个问题。2.3 增强链路的实测对比为了让你有个直观参照我把几种常见方案在我们自采的验证集上跑了一遍。测试集包含 300 帧覆盖三档水体、两个拍摄距离档位0.6 m 和 1.5 m指标是分割 mIoU 和中心线偏移 RMS。方案推理耗时ms640×480分割 mIoU中心线 RMScm浑水下的表现原图直接输入00.6124.8严重退化几乎不可用仅 CLAHE3.10.6883.5背景噪声放大边缘毛刺仅暗通道去雾28.60.7013.2蓝色通道被当暗通道偏色严重红补偿 白平衡 CLAHE5.40.7642.1稳定可控生成模型增强方案2100.7712.4效果好但时延不可接受暗通道去雾那一行值得多说两句。理论上它很适合处理散射造成的雾但水下场景有个坑暗通道先验假设无雾图像中总存在某个通道的局部最小值接近 0而水下图像的蓝通道本身全局就偏亮红通道又几乎全黑这个假设直接被违反估出来的透射率图会全图偏小去雾之后画面反而更暗更偏红。我试过加颜色空间转换转 Lab 只在 L 通道做能缓解一些但收益不值那个复杂度最后放弃了。生成模型方案效果确实好但 210 ms 的耗时意味着整条链路只能跑到 45 fps控制环根本吃不上。在算力有限的机载平台上这个方案目前只能作为离线的图像美化工具不能进实时闭环。3. 管道本体提取从像素分割到轴线拟合3.1 分割模型选型轻量优先类别定义比网络结构更重要管道本体提取我走了分割 几何拟合这条两段式路线而不是直接用一个检测框回归。原因很简单管道在图像里是一条贯穿画面的长条状目标检测框会把大量背景框进来中心点估计误差极大而分割出来的 mask 可以逐列取中点得到亚像素级的轴线。网络用的什么试过 DeepLabv3、UNet、以及 YOLO 的实例分割版本。在 640×480 输入、机载边缘板卡上跑UNet以轻量主干为编码器的性价比最高mIoU 0.764单帧耗时 22 ms。DeepLabv3 精度高一点点0.771但耗时翻倍不划算。实例分割版本速度快但 mask 边界精度不如专门的语义分割网络管道边缘会有 12 像素的锯齿对拟合不利。真正决定成败的其实是类别定义。我第一版定义了 6 类管道、法兰、支架、附着物、裂缝、背景。训出来之后发现裂缝这一类几乎学不会召回率不到 30%。分析下来是两个原因裂缝像素占比太小全图不到 0.1%以及裂缝的视觉特征和管道表面的划痕、水渍高度相似。后来拆成两个独立通道分割网络只负责分 3 类管道、背景、其他结构件裂缝单独用一个检测网络去抓。这一拆分割的 mIoU 从 0.71 涨到 0.764类别少了类间混淆也少了裂缝的召回率从 30% 提到 68%。这个经验我觉得挺通用当某一类的像素占比低一个数量级以上时别硬塞进分割网络换检测通道。3.2 轴线拟合为什么最后用了 RANSAC 而不是最小二乘拿到 mask 之后常规做法是逐列找 mask 的上下边界取中点作为该列的中心点然后用最小二乘拟合一条曲线。我一开始就是这么干的结果在有两处失效一是 mask 上有小孔洞比如被附着物遮挡中点直接跳到孔洞边缘二是机器人转弯时管道在画面里是弯的二次曲线拟合对局部异常点非常敏感一个坏点就能把整条曲线拽歪。换成 RANSAC 之后情况好了很多。具体做法import numpy as np from sklearn.linear_model import RANSACRegressor from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline def fit_pipe_axis(mask, min_col_pixels8): h, w mask.shape cx_list, cy_list [], [] for x in range(w): col np.where(mask[:, x] 0)[0] if col.size min_col_pixels: continue # 该列有效像素太少跳过 # 检查该列是否为连续段断开则跳过 if col[-1] - col[0] 1 ! col.size: continue cx_list.append(x) cy_list.append(col.mean()) if len(cx_list) 30: return None # 有效列不足本帧不输出 X np.array(cx_list).reshape(-1, 1) y np.array(cy_list) model make_pipeline( PolynomialFeatures(degree2), RANSACRegressor(min_samples0.6, residual_threshold3.0, max_trials200) ) model.fit(X, y) return model几个参数的含义和我的取值理由residual_threshold3.0是像素单位意思是偏离拟合曲线 3 像素以上的点判为外点。这个值不能太小因为 mask 边界本身就有 12 像素的抖动也不能太大否则真正的弯曲特征会被当噪声抹平。「该列是否连续」这个检查非常关键它把被遮挡产生的孔洞列直接剔掉了比任何后处理都有效。拟合出中心线之后取图像中心列 x w/2 处的曲线值 y_c与图像中心 y h/2 求差就得到横向偏移量像素。这个值再经过标定换算成物理量喂给控制。注意管道在画面里不是水平的时候你用图像中心列去取点其实取的是斜线上的一个点横向偏移的物理含义会打折。我的处理是先根据拟合曲线的首尾点算出一个旋转角把图像做一次仿射旋转到管道水平再做偏移计算。这一步不做的话机器人转弯时会持续向一侧偏。3.3 缺陷检测通道小目标策略和置信度阈值缺陷检测用的是 YOLO 系列的单阶段检测器输入 640×640模型选的是 n 或 s 级别的轻量版本。在边缘板卡上跑到 2535 fps够用。这里真正的功夫不在模型在数据增强和锚框尺度。裂缝这类目标在 640 输入下可能只有 4×40 像素标准的 Mosaic 增强会把它们缩得更小训出来效果很差。我的做法是关闭 Mosaic改用大比例的随机裁剪缩放scale jitter 0.61.4保证小目标在训练时尺寸分布和推理时接近提高小目标的采样权重对面积小于 32×32 的标注框做 2 倍重复采样置信度阈值不能按常规来定。默认 0.25 的阈值在我们这个场景下误报率约 15%把锈斑判成裂缝我调到 0.45 之后误报降到 4%漏检上升 3 个百分点。考虑到巡检场景宁可少报也别乱报因为每一条报警都要人工复核这个取舍是划算的。另外补充一点现场经验补光灯的开关状态对检测结果影响极大几乎可以认为是两个不同的域。我的做法是在训练集里强制掺入 30% 的无补光样本让模型对这个变量不敏感。如果在部署时才发现这个问题返工成本会很高。4. 从像素到动作视觉与控制的对接细节4.1 单目测距的标定流程以及为什么必须装配后标定管道直径已知比如 200 mm那么用相似三角形就能反推距离def estimate_distance(pipe_width_px, focal_px, pipe_real_width_m): # 距离 焦距 * 实际宽度 / 像素宽度 return focal_px * pipe_real_width_m / max(pipe_width_px, 1.0)公式三行就写完了难点全在focal_px怎么来。它不能直接用相机出厂参数算因为相机装进了防水舱隔着一层厚玻璃看外面平板玻璃在水下会引入额外的折射等效焦距和空气中不一样而且视场角会被压缩典型的压缩幅度在 5%15%取决于玻璃厚度和舱内空气腔的尺寸。我的标定流程是这样的相机装进舱体、密封、下水在最终装配状态下拍摄标定板。标定板要防水或者封装在透明袋里放在水里。用棋盘格角点检测算内参得到focal_px。用已知宽度的标尺在不同距离0.4 / 0.8 / 1.2 / 1.6 / 2.0 m各拍 10 张测像素宽度反算距离画距离-误差曲线。如果误差随距离呈系统性偏大或偏小说明存在折射导致的比例误差做一个线性修正系数k让focal_px_eff k * focal_px。我测出来的k是 1.07也就是说如果不做这个修正2 米处测距会偏大 14 cm 左右。这个量级对于管道巡检的避障判断来说是致命的——你会以为还有距离实际上已经贴上去了。提示标定务必在最终装配、最终密封、最终下水状态下做。我见过有人用空气中的标定参数直接下水跑误差大到自己都不信。装配状态一变换个玻璃、调个镜头位置标定全部作废。4.2 帧率、延迟与流水线拆分视觉链路的总延迟不是各环节耗时简单相加得看流水线怎么排。测过一次完整链路的时间分布640×480边缘板卡环节耗时相机曝光 传输33 ms30 fps 相机预处理红补偿白平衡CLAHE6 ms分割推理22 ms后处理 轴线拟合4 ms检测推理隔帧执行30 ms结果打包发布2 ms不隔帧的情况下总计约 97 ms。这个延迟意味着如果机器人以 0.4 m/s 前进从拍到图到输出结果位置已经前移了将近 4 cm。对于缺陷定位这个量要补偿掉在输出缺陷坐标时用当前速度乘以延迟时间做前向补偿。更关键的是控制环根本不能等视觉。我的架构是视觉节点以 10 Hz 发布中心线偏移和距离控制节点以 50 Hz 运行中间用一个轻量卡尔曼滤波做状态外推。视觉效果好的时候滤波主要做平滑视觉丢帧的时候滤波做短时预测最多支撑 300 ms超过这个时间就降级到定深直行 减速。这个兜底策略在实际测试中救过好几次——水草遮挡导致连续几帧分割失败如果没有外推机器人会直接朝一侧冲出去。4.3 光照突变、遮挡与丢帧的兜底策略真实水体里光照是会突变的。水面波纹聚焦阳光、机器人自身的补光灯在转弯时扫过舱壁、悬浮物突然飘过镜头都会造成画面亮度在几帧内跳变几十个灰度级。我的处理是在预处理之后加一个质量门控计算图像的平均梯度和有效分割面积的比值如果分割出的管道面积占全图比例小于 8%或者轴线拟合的有效列数少于 30本帧结果标记为不可信不发布连续 3 帧不可信触发状态机降级如果检测到整图平均亮度比滑动均值偏离超过 40%跳过当前帧的增强参数沿用上一帧的参数避免参数跟着噪声乱跳。这套门控写起来不到 50 行但它是整个系统从能跑到敢让它自己跑的分水岭。实验室里演示的时候你可以人工盯着出了问题手动接管真让它自己跑十几分钟没有兜底是绝对不行的。5. 数据集最花时间、最容易翻车的一环5.1 水池数据和真实水体的域差异有多大我们自采的数据集构成大致是这样水池数据 1800 张清水、瓷砖背景、中心补光真实水域数据 700 张微浑、泥沙底、自然光另外还有 300 张是故意在最恶劣条件下拍的难例。标注全部用多边形分割加框双人交叉复核。域差异有多夸张我做过一个对照实验只用池数据训在真实水域测分割 mIoU 从 0.78 掉到 0.51。掉得最狠的是管道边缘——水池里管道是深色圆柱贴着浅色瓷砖边缘极其锐利真实水体里管道表面附着一层生物膜颜色和背景水体接近边缘几乎消失。缩小差距的手段按性价比排序补真实数据哪怕只有两三百张效果也比任何域适应算法都好。这是第一优先级。在池数据上做合成退化按物理模型给池数据加上随机的蓝绿偏移、对比度衰减、雾状叠加模拟不同浑浊度。这一步把 mIoU 拉回到 0.66成本几乎为零。强增强颜色抖动、随机伽马、模糊、噪声这些常规手段也有用但边际收益递减很快。5.2 标注规范一致性比数量重要标注环节我踩过一个很典型的坑两个人标同一批图一个人把管道表面所有的深色斑点都标成缺陷另一个人只标长度超过 3 cm 的。结果训练出来的模型学了个中间态谁也不像。后来定了一份硬性规范关键几条缺陷最小标注尺寸在标准拍摄距离0.8 m下长度小于 15 像素的不标附着物只要覆盖面积超过管道投影面积的 10% 才标整体否则只标显著部分管道与结构件的分界以几何连接处为准不按材质颜色分模糊到人眼无法判断的样本直接剔除不要勉强标。规范定下来之后标注速度反而快了因为不用每张图纠结。我建议在开标之前先让所有参与的人一起标同样的 30 张然后交叉比对把分歧点逐条讨论清楚写进规范。这半天时间能省掉后面几天的返工。还有个提效技巧先训一个粗糙模型用它预标注人工只做修正。当模型 mIoU 到 0.6 左右的时候预标注能省掉大概 40% 的人工时间。但要注意模型本身的系统性错误会被标注者继承下来所以每批预标注里要掺 10% 的纯手工标注做对照一旦发现偏差扩大就停用预标注重训。6. 返工三次才想明白的几个细节6.1 防水舱玻璃带来的视野畸变比想象中严重前面提过折射影响测距其实它对分割的影响同样大。平板玻璃在水下相当于一个负透镜会让画面边缘产生桶形畸变而且这个畸变和相机本身的镜头畸变叠加在一起不是简单的径向畸变模型能拟合的。我最初只用相机内参做去畸变结果画面中心区域很准边缘的管道轴线拟合出来会向外弯。后来改用多项式拟合的畸变查找表用一组网格标定板在水下拍摄逐点建立畸变坐标 → 理想坐标的映射用cv2.remap做查表校正边缘误差从 6 像素降到 1.5 像素以内。这一步的代价是每帧多花 34 ms但值得。如果你的画面边缘也用得上管道常常从画面边缘穿出这个校正躲不掉。6.2 补光灯频闪和卷帘快门的组合坑补光灯如果是 PWM 调光的 LED在卷帘快门下会拍出横向明暗条纹。这个条纹对人眼几乎不可见因为频率高但对分割网络的干扰极大——网络会把明暗交界线当成管道边界轴线直接跳变。排查过程挺费劲的。最开始怀疑是水体波动换了静止水箱还有怀疑是相机问题换相机还有最后用高速摄影看补光灯才发现是频闪。解决方式两个一是把补光灯改成恒流驱动去掉 PWM 调光二是把曝光时间设成工频周期的整数倍我们这边是 50 Hz取 20 ms 的整数倍。两个一起用条纹彻底消失。如果只能选一个优先改驱动方式因为曝光时间拉长会牺牲动态场景的清晰度。6.3 评测指标好看但现场表现差问题出在哪这个坑我觉得最值得单独说。模型在验证集上 mIoU 0.76、缺陷 mAP 0.72看着挺体面实际下水跑的时候操作的人反馈老是漏报。排查下来是验证集的分布和现场不匹配。验证集是随机从全部数据里切的而实际运行时机器人大部分时间在拍没有缺陷的正常管道只有少数时间经过缺陷段。也就是说现场遇到缺陷的样本比例远低于验证集。这种情况下模型在验证集上的表现被大量简单样本拉高了。改法很简单按缺陷尺寸分档重新统计。把缺陷按像素面积分成小32²、中32²96²、大96²三档分别算召回。结果一目了然大缺陷召回 91%中缺陷 74%小缺陷只有 38%。现场反馈的漏报全部落在小缺陷档。定位问题之后对策也清楚了针对小缺陷单独做一次过采样重训同时把输入分辨率从 640 提到 800代价是推理时间从 22 ms 涨到 31 ms可以接受小缺陷召回提到 61%。这个数字依然不算好但至少不再是个黑盒——你知道它的边界在哪可以在报告里如实写清楚而不是给个虚高的总指标。提示任何一个视觉指标只要它没有按目标尺寸、拍摄距离、水体浑浊度分档统计就不能拿来判断系统能不能用。整体指标是给评审看的分档指标才是给自己看的。最后分享一个我觉得挺有用的小习惯每次下水测试不管成功失败都把原始视频原封不动存下来按日期和测试条件建目录。我后期做域适应、调参、复现 bug全靠这些原始素材。有一次一个诡异的轴线跳变查了两天最后在三个月前的一段旧视频里找到了同样的现象一对比就发现是那个批次的补光灯驱动板有问题。这些素材的价值在采集当时是看不出来的但它是你后面所有分析的唯一依据。存储成本相对于整个项目的投入真的不值一提。