偏振成像水下图像去雾:从物理模型到MATLAB实现
1. 先搞清楚水下图像为什么会变“雾”1.1 水的吸收与散射才是罪魁祸首我最早接触水下图像去雾是因为手头一个水下巡检项目——某海上平台结构物的安全检测要拿水下机器人拍的视频去做缺陷识别。结果发现水下图片和雾天图片在视觉上很相似都是白茫茫一片、细节丢失、颜色发灰。但你如果真拿雾天去雾的算法去处理水下图效果往往很差甚至会越处理越糊。这里面的原因得从光的传播机制说起。水下成像的退化本质上是两条“凶手链”叠加的结果。第一条是吸收。水对不同波长的光吸收效率完全不一样——红光在几米内就衰减得差不多了蓝绿光能传得远一些。这也是为什么水下照片总是偏蓝偏绿。吸收导致的是能量损失信号变弱相当于照度不足的暗光成像。第二条是散射。光子在水里传播时会和水分子、溶解的有机物、悬浮颗粒发生碰撞改变方向。这里面又分成两类前向散射是目标反射的光在到达相机前被散射掉一部分造成图像模糊像是给画面蒙了一层毛玻璃后向散射则是环境光天空自然光、照明灯光在没到达目标之前就被水里的微粒散射回相机方向。后向散射光相当于无数个“小闪光灯”同时对着镜头打光直接抬高了图像的底部亮度水平形成像雾一样的面纱。你在岸上拍雾气大概也是吸收加散射的共同作用但有一件事在水下特别突出后向散射光是带有明显偏振特性的一束光。它经过一次或者多次散射之后光的振动方向会呈现出明显的取向性。而目标反射回来的直接光大概率还是非偏振光或者偏振度极低。这种偏振状态的差异就是我们做偏振去雾的核心切入点。为了验证这件事我当时做了一个小实验在实验室水箱里放一个黑白棋盘格标定板用同一台相机分别拍两张图一张经过偏振片最大透振方向一张经过最小透振方向。对比两张图的背景区域你会发现偏振差分图的背景亮度和目标区域差异非常明显。这个实验做完你就自然明白“偏振模型为什么能反演后向散射”了。1.2 偏振信息相当于把“雾”单独拎出来一句话概括偏振去雾的思路想办法估计出那层后向散射面纱然后从观测图像里把这层面纱减掉再做透射率补偿。常规RGB图像去雾算法只能靠“暗通道”之类的统计先验去猜面纱而偏振方法因为多了一路偏振维度可以更直接地测出面纱的强度分布。打个比方普通的彩色照片相当于你闭着眼睛凭感觉判断房间里哪里有一层烟雾偏振成像相当于你戴了一副能把这层烟雾“标色”的特殊眼镜烟雾在你眼里变成了半透明的紫色目标的颜色却基本不变。有了这个标记你就可以精准地把烟雾擦掉。但这里有一个关键前提你必须在采集端就做好准备。偏振去雾依赖两个正交偏振方向的图像也就是旋转偏振片到两个相差90度的透振方向分别曝光采集。如果项目里只能用已经拍好的普通照片那偏振方法就用不了只能退回去用暗通道或者深度学习。这也是很多新手栽跟头的地方——以为拿到了MATLAB代码就能处理任意一张水下图片实际上偏振方法是一个“采集端算法端”的整体方案光有后半段是不够的。所以如果你打算完整复现这个项目我建议你先确认自己的图像数据来源最好按下面的标准去准备能访问原始采集环境或者你自己控制相机拍摄相机前面能加装可旋转偏振片线偏振片就行不一定需要圆偏振同一个场景至少有两张偏振方向正交的帧且场景静止、光照条件一致如果项目已经到了算法验证阶段也可以先用公开的偏振水下数据集或者自己用水箱加偏振片补拍几组。拿到图像对之后的事情就是我们下面要说的物理建模和MATLAB代码了。2. 偏振物理模型的数学骨架从Stokes参数到去雾公式2.1 用Stokes参数描述偏振状态偏振物理模型的书本版本比较劝退一上来就是电动力学推导、穆勒矩阵容易让人看不下去。但实际工程实现一般只需要用到强度层面的简化模型。这里我推荐把光强拆成三个量I总强度等价于不带偏振片拍到的图像亮度Q水平偏振分量与垂直偏振分量之差U45度偏振分量与-45度偏振分量之差(I, Q, U)就是Stokes矢量中常用的前三个参数。对于线偏振片采集你旋转偏振片得到强度 I(θ)数学上满足I(θ) (I Q·cos 2θ U·sin 2θ) / 2这意味着理论上拍三个不同角度的强度图就能解出 I、Q、U。但在水下去雾里我们一般简化成拍两个正交方向即θ0°和θ90°。于是有I_parallel (I Q) / 2 对应最大透振方向I_perpendicular (I - Q) / 2对应垂直方向那么总强度 I I_parallel I_perpendicular差分 DOP_map (I_parallel - I_perpendicular) / (I_parallel I_perpendicular)这个DOP就是线偏振度。你可能会问实际中U分量要不要管水下环境的后向散射光主偏振轴通常跟入射光的偏振轴一致而环境光以自然光为主散射之后偏振轴基本固定。我们只需要把偏振片调到最大透振方向记下这一帧为I_max再旋转90度得到I_min。两帧就够了U分量的影响会体现在I_max和I_min的取值里不需要单独求解。2.2 后向散射估计与图像恢复公式偏振去雾模型的思路源自大气散射模型只是把后向散射进一步拆解。观测图像 I_obs(x) 写成两部分之和I_obs(x) D(x) B(x)其中 D(x) 是目标反射光的直接传输分量B(x)是后向散射分量。D(x) 进一步写为 J(x)·t(x)J(x)是没有雾的清晰辐射t(x)是透射率。B(x) 则近似写成 A_inf·(1 - t(x))A_inf是无穷远处的背景光强度。偏振假设的核心是D(x)基本是非偏振的也就是在I_max和I_min两个方向上各占一半而B(x)是部分偏振的两部分强度不一样。于是I_max(x) D(x)/2 B_max(x)I_min(x) D(x)/2 B_min(x)把两式相减D项抵消得到偏振差分ΔI(x) I_max(x) - I_min(x) B_max(x) - B_min(x)再引入一个背景光偏振度的概念定义为远处背景区域里两幅偏振图差值与总强度的比值p_B (B_max - B_min) / (B_max B_min) ΔI_B / (2·B_inf)如果假设全图的背景光偏振度近似等于背景区域估计的 p_B那么每个像素的后向散射强度可以估计为B(x) ΔI(x) / p_B有了B(x)透射率就顺手算出来了t(x) 1 - B(x) / A_inf最后恢复图像J(x) [I_obs(x) - B(x)] / t(x)括号里是去面纱操作除以t(x)是补偿衰减。这个流程看着简单实际做起来有几个细节非常影响效果后面我会逐个说明。公式推导完成后你会发现整个算法根本不依赖任何机器学习训练纯粹是线性代数加物理假设。好处是稳定、可解释、计算量小一台普通笔记本跑MATLAB处理百万像素级图像单帧速度足够快坏处是假设一旦不成立结果就会退化。比如目标本身有镜面反射反射光带偏振D项的偏振假设就失效了去雾之后目标区域会出现过曝或暗斑。3. MATLAB实现一步一步把模型变成能跑的代码3.1 图像读取与预处理建议的数据组织方式是这样的把两个偏振方向的图像命名成 im_max.png 和 im_min.png放同一个文件夹。读图之后先做配准——实际拍摄时旋转偏振片可能会导致微小位移建议用MATLAB的 imregcorr 做一次相位相关配准。不过大部分实验室场景下相机不动旋转偏振片造成的偏移很小可以直接跳过。下面是读取和基础预处理的代码% 读取正交偏振图像 I_max im2double(imread(im_max.png)); I_min im2double(imread(im_min.png)); % 如果是灰度图保持如果是彩色图先转灰度做物理量估计 if size(I_max, 3) 3 I_max_gray rgb2gray(I_max); I_min_gray rgb2gray(I_min); else I_max_gray I_max; I_min_gray I_min; end % 轻微高斯平滑压制传感器噪声 gauss_filter fspecial(gaussian, [5 5], 1.2); I_max_gray imfilter(I_max_gray, gauss_filter, replicate); I_min_gray imfilter(I_min_gray, gauss_filter, replicate); % 总强度和偏振差分 I_total I_max_gray I_min_gray; I_diff I_max_gray - I_min_gray;有一个简单但容易犯的错误直接用相机自动曝光拍两帧偏振片一旋转自动白平衡和自动曝光会改变整体亮度导致两帧的亮度水平不一致最后算出的偏振差分全是虚假信号。我在实际项目里吃过这个亏——拍出来的差分图像花成一片背景区域根本挑不出来干净的采样点。解决办法是固定曝光参数或者对两帧做一个全局线性直方图匹配把I_min的整体亮度水平对齐到I_max上。当然最稳妥的还是拍摄时就用Raw格式加手动曝光从源头消除这个麻烦。3.2 背景光区域与偏振度的估计估计背景光是整个算法里最敏感的一步。背景光区域应该是画面里没有目标、且离相机足够远的那片水体。水下图像里它通常对应于图像上方的水体区域或者图像中最亮的模糊区域。经验做法是取图像顶部5%到10%高度的条带或者手动用矩形框选一块没有目标的水域。拿到背景区域后估计背景光强度 A_inf 和偏振度 p_B% 手动选区或自动取顶部区域 [rows, cols] size(I_total); bg_region 1:round(rows * 0.1); % 默认取顶部10%区域 A_inf mean(I_total(bg_region, :), all); BG_max mean(I_max_gray(bg_region, :), all); BG_min mean(I_min_gray(bg_region, :), all); p_B (BG_max - BG_min) / (BG_max BG_min eps); % 防止偏振度过小导致除零给一个下限保护 p_B max(p_B, 0.05);这里有三个坑要讲清楚。第一个坑是A_inf不能用全图的灰度最大值。水下场景里目标如果有高反光材料其亮度完全可能超过背景水体直接用最大值会把A_inf拉高导致透射率算出来偏大恢复图像暗部死黑。我在实验中发现取背景区域的高分位点比如95%分位数比取均值更稳因为纯水体区域噪声波动比较小。第二个坑是p_B的下限保护。如果水质太清澈背景光偏振度很低差分信号微弱p_B会接近零B(x) ΔI(x)/p_B 就会爆炸。给个0.05的下限能防止数值发散但要注意这是权宜之计实际效果还需要靠后面的t(x)裁剪来兜底。第三个坑是“全图等偏振度假设”带来的误差。真实场景里p_B随空间位置变化尤其是照明不均或者悬浮物浓度不均时。你会发现恢复出来的图像有一侧发亮一侧发暗就是因为B(x)被低估或高估了。遇到这种情况可以考虑把背景区域分割成多个子块逐块估计p_B再做平滑插值。但平滑插值这块操作起来有点麻烦工作量会明显上来如果你的场景比较简单先不用升级到这一步。3.3 透射率估计与图像恢复透射率的计算还有两个细节需要处理。一是B(x)和A_inf做了减法之后可能出现负值导致t(x)大于1或者直接除出爆亮的高光点二是恢复后的J(x)噪声会被放大特别是暗部区域信噪比本来就低除以小透射率数值之后噪声呈指数级增长。% 估计后向散射 B_est I_diff ./ (p_B eps); % 背景光估计用背景区域的95%分位点 A_inf prctile(I_total(bg_region, :), 95, all); % 透射率 t_map 1 - B_est ./ (A_inf eps); % 限定透射率范围避免除零和灰白边 t_map min(max(t_map, 0.1), 1); % 恢复清晰图像 J_est (I_total - B_est) ./ (t_map eps); % 亮度截断和拉伸改善显示效果 J_est max(min(J_est, 1), 0);做完这一步你基本能看到去雾后的图像了。如果直接显示这段代码的输出结果往往偏灰、偏暗。原因有二一是J(x)的灰度范围经过减法和除法后被压缩二是水下蓝绿光的后向散射被移除后色彩通道的灰度水平产生了偏移需要做后处理。接下来我给的增强处理只是经验性步骤不是物理模型的一部分但对最终视觉效果的提升非常明显% 直方图截断拉伸提升对比度 low_val prctile(J_est(:), 2); high_val prctile(J_est(:), 98); J_stretch imadjust(mat2gray(J_est), [low_val high_val], [0 1]); % 简单的灰度世界色彩校正如果原图为彩色 if size(I_max, 3) 3 J_color zeros(size(I_max)); for c 1:3 channel (I_max(:,:,c) I_min(:,:,c)) - B_est; channel channel ./ (t_map eps); channel max(min(channel, 1), 0); channel imadjust(channel, [prctile(channel(:), 2) prctile(channel(:), 98)], [0 1]); J_color(:,:,c) channel; end end灰度图版本到拉伸这一步就可以收工了彩色图版本建议做一次白平衡。因为水下红光衰减远大于蓝绿恢复后的彩色图像通常还是蓝绿色主导灰度世界假设每一通道均值相等可以做一个简单的通道增益校正。不过这种校正会把红色的占比强行拉高有时候会偏红具体增益系数还得根据水质微调。为了让你对参数有个直觉我把自己调试时用过的一组典型参数表格放在下面。不同水体环境差异很大这组参数适合近海中等浑浊水体淡水清澈水体要适当调低p_B下限和透射率下限。参数典型取值调参方向物理含义p_B下限0.05目标偏亮时适当增大防止背景光偏振度估计过低t_map下限0.1噪点明显时增大防止暗部噪声放大背景区域高度10%行数目标区域上移时缩小确保采样点落在纯水体高斯平滑核5x5, sigma1.2噪声大时加大压制传感器热噪声A_inf分位数95%水体亮度高时用90%避免高亮噪声拉高背景4. 参数敏感性与效果评价方法4.1 核心参数对恢复结果的影响实验我在跑通初版代码之后专门做了一轮参数敏感性测试在固定的水下图像对上一项一项地改动参数并记录输出。实验结论大致是这样的。背景区域高度对结果的影响最大。如果框选的背景区域不小心纳入了目标的边缘A_inf和p_B都会被污染恢复结果在对应位置会出现明显的暗色伪影像是一团脏水糊在物体表面。这比p_B稍微偏小带来的影响严重得多。所以如果工程上允许我强烈建议做一个人工ROI选择界面只用鼠标框一次背景比任何自动算法都可靠。p_B下限从0.01改到0.1恢复图像的整体亮度变化趋势是单调的——p_B越大估计出的B(x)越小透射率越大恢复图越暗。这是因为B(x)被低估后t(x) 1 - B/A_inf 偏高除法补偿不足。想要亮一些的去雾结果就应该用小一点的p_B想要保守、少一些噪声放大就稍微加大p_B。实际项目中我一般用0.08附近作为起步值。t_map下限控制的是最暗区域的补偿强度。下限设得越低暗部细节越突出噪声也越明显。比较典型的现象是t_map下限为0.01时图像底部或者角落会出现颗粒状彩色噪声设为0.2之后画面干净很多但暗部细节也会跟着丢失。如果你处理的是后续要送进目标识别网络的数据建议把下限往大调识别模型对噪声的忍受能力往往低于对细节丢失的忍受能力。这套参数调节逻辑和传统图像处理里的“锐化强度”“去噪强度”之类很不一样——它每个旋钮都对应一个物理量调节方向有明确含义。你调参的时候最好心里始终记着三条线A_inf控制的是“天空有多亮”p_B控制的是“面纱有多偏振”t_map下限控制的是“最远能补偿到多深”。这比黑盒调参要容易得多。4.2 定量评价指标选择调完参数总不能只靠肉眼说“看起来变清楚了”。项目汇报或者论文里都需要定量指标。我试过几组指标这里按照实用性排个序。结构相似度SSIM是最常用也最好解释的指标。如果项目有参考清晰图像比如水箱实验前拍一组清水状态下的图SSIM能直接比较恢复图和真实清晰图的亮度、对比度、结构相似程度。提升幅度通常在0.1到0.3之间具体取决于水体浑浊程度。图像信息熵用来衡量纹理丰富程度。去雾之后熵值一般会上升说明灰度层次变多了这是细节恢复的间接证据。但要注意过度增强也会提高熵值所以信息熵只能做辅助指标不能单独证明算法有效。水下专用指标UIQM也好用它是基于色彩、对比度和清晰度的加权组合不需要参考图像比较合适野外实测场景。缺点是权重参数需要自己攒经验去设定不同水体的最优权重不同。我个人的做法是论文汇报用SSIM加UIQM工程调试时主要看肉眼的边缘清晰度和颜色自然程度指标只是辅助。指标是否需要参考图计算成本适用场景SSIM需要低实验室水箱定量验证信息熵不需要低快速评价细节丰富度UIQM不需要中野外水下实测PSNR需要低与经典算法对比时使用5. 踩坑实录与工程化改进建议5.1 偏振片选择与采集端失误最后一个部分想把我实际踩过的坑集中写下来这些在论文里一般不会出现但能帮你省下好几个周末。偏振片选型上我一开始随便买了一片劣质线性偏振片旋转的时候透振方向刻度盘也不准。结果拍出来的两帧图像亮度差很高算法处理完出现横跨整张图的条纹状不均匀后来才发现是偏振片本身厚度不均匀引入了额外相位延迟。建议至少购买透射率均匀性好的专业级线偏振片并且在拍摄前做一个简单的平行光管检验把偏振片放在均匀光源前旋转观察输出亮度是否平滑变化。曝光参数必须锁定。第一次采集数据时我让相机处于自动模式拍完发现I_max和I_min的平均亮度差有20%以上这个不等性直接被算法当成偏振差分信号去雾之后水体区域出现大面积的虚假高光。解决方法是手动设置曝光时间、光圈、ISO和白平衡并且用遥控快门或者软件间隔拍摄避免手按快门造成机震。还有就是目标运动的问题。水下是动态环境鱼、浮游生物、气泡都可能进入视野。偏振模型假设场景静止如果有目标在两张偏振帧之间运动差分图像会同时包含真实的偏振差分和运动伪影去雾结果在运动区域会出现重影。项目场景里如果无法避免运动目标一个折中方案是拍摄时把曝光时间尽量缩短帧间隔尽量拉小从段时间上减少目标位移量。更激进的做法是改用分光方式同时获取两个偏振方向但消费级方案的齐偏振相机价格比较高预算有限时不推荐用双帧方案配合限速采集就够了。5.2 从MATLAB原型到实时系统的扩展路径MATLAB原型适合验证算法正确性但它处理一帧约百万像素图像在我的测试机上单帧大概要300到500毫秒主要瓶颈是imfilter和逐像素运算。如果项目目标是实时去雾需要做几个层面的优化。先做算法层面的减法。背景区域估计、A_inf和p_B计算可以只在第一帧初始化之后每隔几十帧更新一次中间继续沿用旧值。因为水下光照和悬浮物浓度变化相对缓慢这一假设在多数场景下都是成立的。这样省掉了每帧的背景区域统计运算省下的时间相当可观。再做代码层面的加速。把灰度转换、差分、透射率计算这些像素级操作从MATLAB循环改成矩阵运算已经是最基础的优化。进一步可以把核心函数写成MEXC语言编译成MATLAB插件实测提速五到十倍。工具链成熟而且调试不复杂代价是你得维护两份代码。再更进一步如果部署到嵌入式或者工业相机上直接用C加SIMD指令优化矩阵运算然后把算法移植到GPU上。偏振差分和透射率估计都是元素级操作天然适合并行。整体流程设计成相机输出两路偏振帧GPU上做配准、去雾和色彩校正再送去显示或者识别模块在中等性能的嵌入式GPU上实现30帧每秒是完全可行的。我这边的项目后来就是把MATLAB原型里的核心参数固化到一套C模块里做成了水下检测系统的预处理链路。实测下来加了这个预处理步骤之后缺陷检测的误报率下降了不少。这说明一个物理模型扎实的去雾算法在工程项目里的价值不只是“让图变好看”而是实打实地改善下游任务。这也是我一直愿意往深里做偏振方向的原因——只要采集端配合到位这套方法在真实场景里的稳定性和可解释性确实比很多黑盒数据驱动方法更让人放心。最后分享一个调参小技巧开发阶段准备三组典型输入一组清澈水体一组中等浑浊一组重度浑水。每次改完代码三组图都跑一遍分别记录SSIM和UIQM。任何一组指标明显变差说明改动可能过拟合到了某一类场景。拿这三组图当回归测试比每次只看一张图效果好得多。这个习惯我从这个项目一直沿用到现在算是亲身验证过最有价值的工作方法了。