偏振成像水下增强:用偏振差分物理去除后向散射 简介基于物理模型的水下偏振成像增强Matlab代码包面向电子信息、计算机、数学等专业学生适用于数字图像处理课程设计、计算机视觉期末大作业或毕业设计中的图像复原场景。针对水下图像对比度低、颜色失真、雾化严重等问题代码以穆勒矩阵与斯托克斯矢量描述偏振信息通过分离目标反射光与路径散射光实现去雾与色彩恢复并支持不同水深及不同水质类型数据。压缩包共15个文件以8个m脚本为核心辅以5个bmp真实样本、1个png效果预览图及1个md说明文档整体大小仅599KB目录结构清晰主程序、config配置与去散射子函数等模块化编排参数集中管理仅需修改config.m即可切换场景各函数附中文注释便于掌握偏振差分与去雾强度图等关键计算。配套实拍案例数据可直接运行输出增强图同时生成PSNR、SSIM等客观评价指标。已有38人学习浏览适合需要可复现、可扩展偏振成像复原方案的研究者与开发者。1. 偏振成像做水下增强不靠猜雾的物理派解法水下拍十次有九次得到的画面是一层蓝绿色的雾。这层雾不是对焦问题而是水体里悬浮颗粒对光的后向散射——散射光混在目标反射光里一起进镜头把对比度压到几乎为零。水下图像增强要做的就是把散射光从画面里精确剥掉。常见的增强做法是纯图像层面的拉伸和颜色校正见什么拉什么结果时好时坏而基于物理的水下图像增强先问一句散射光能不能被直接测出来偏振成像恰好回答这个问题——散射光带有明显的偏振特性目标反射光却接近退偏振于是用两幅正交偏振图像做一次差分就能估算出散射光强度并从总光强中减去。这个方法适合手里有偏振相机或两幅正交偏振图的从业者比如水下机器视觉、水下检测和潜水摄影。它不需要训练数据计算量也不大单张图在普通 CPU 上跑完只要几十毫秒。2. 偏振为什么能把散射光从画面里分离出来2.1 水下那层“雾”到底从哪来光在水下走不远。一方面水体会吸收光能红光在纯水里每米的衰减可以到 0.4 量级绿光和蓝光好得多大约在 0.02 到 0.05 量级所以阳光照进水里没多深剩下的主要是蓝绿色这就是水下画面整体偏蓝绿的原因另一方面水中的悬浮颗粒、微小气泡和溶解物会把光推向四面八方。前向散射是目标反射光在到达相机的路上被打散效果是画面发糊、细节边缘变软后向散射是照明光在进入相机的光路上直接被颗粒弹进镜头效果是一层均匀的雾状光罩在画面上越浑浊越明显。偏振方法要处理的主要对象就是后向散射因为它在所有污染项里占比最大、也最影响对比度。后向散射在成像模型里是一个加性项相机接收到的总光强 I S DS 是目标反射光信号光D 是后向散射光。D 叠加在 S 之上相当于给整幅图加了一个不均匀的直流偏置把动态范围压低对比度自然就上不去。衰减系数和散射强度都随波长变化这可以整理成一张常用的参考表波段纯水中衰减系数量级对水下图像的影响红光 620~750nm约 0.4 /m深度稍大就消失红色通道值趋近于零绿光 500~570nm约 0.05 /m能走较远主导中距离色彩蓝光 450~500nm约 0.02 /m穿透最远远距离画面整体偏蓝实际水域里藻类、泥沙还会把曲线拉得更歪这个表只用来建立数量级概念不要当成固定参数。做偏振增强时真正关心的是 D 这个加性噪声而不是 S 本身的衰减——S 的损失属于颜色校正的范畴可以在减散射之后再处理。2.2 偏振差分用两幅正交图做减法当光被水中的颗粒散射一次或少数几次时散射光带有明显的线偏振分量而目标表面如果是粗糙的漫反射表面它反射的光在多次反射中把偏振信息抹掉接近自然光没有方向性。于是可以用线偏振片在镜头前采集两个互相垂直的方向平行分量 I∥ 和垂直分量 I⊥。这两个方向的图像就是偏振成像的原始输入。总光强 I I∥ I⊥等价于不加偏振片时拍到的原始图像。偏振差分 Δ |I∥ − I⊥|体现的是场景中“带偏振分量”的光有多少。因为目标信号光近似退偏振它在 I∥ 和 I⊥ 中几乎对半分开对 Δ 的贡献趋近于零而后向散射是部分偏振的在两个方向上强度不同所以差分之后剩下的几乎全是后向散射。把数学写清楚设后向散射在两个正交方向上的强度分别为 D∥ 和 D⊥定义偏振度 p (D∥ − D⊥) / (D∥ D⊥)。实际观测到的偏振差分近似等于 D∥ − D⊥ p·D所以后向散射强度D ≈ Δ / p于是目标信号光S I − D I − Δ / p这个式子就是整个偏振增强的核心。做完这一步等于从每个像素里减去一层估计出来的雾。举个例子背景区域测得 I∥ 180、I⊥ 100则 p (180−100)/(180100) ≈ 0.286该区域 I 280、Δ 80算得 D 80 / 0.286 ≈ 280S ≈ 0说明这个区域确实全是散射光目标区域测得 I∥ 120、I⊥ 90Δ 30用同一个 p 算得 D ≈ 105S ≈ 105目标光从这里被捞了出来。需要强调p 一旦从某个区域估计出来全图都会用同一个值去减。这确实粗糙但它已经能把被雾盖住的对比度拉回一大截。想要继续提升就要处理 p 随距离、随悬浮颗粒变化的问题这在后面参数调优部分会展开。2.3 方法失效的边界偏振目标与浑浊近岸水偏振差分不是万能的。第一个常见失效场景是目标本身带偏振光滑金属、玻璃瓶、部分鱼鳞和塑料表面会产生显著的偏振反射差分后这些目标信号也被当成散射减掉结果目标变暗或出现伪边缘。做水下检测时如果画面里出现这类物体需要先做掩膜保护或者改用多方向偏振重构来判断哪些像素的偏振度异常高。第二个问题是 p 不是全局常数散射光的偏振度随颗粒粒径、散射角和距离变化近处悬浮的大颗粒和远处的细颗粒p 可能差出两倍以上全局用同一个 p 减难免局部过减或欠减。第三个问题在两幅正交图的时间一致性上如果偏振片是旋转采集的两帧之间水的流动、相机的晃动都会让 I∥ 和 I⊥ 不完全对应差分结果就不稳定。针对第二个问题我一般会先做一个快速诊断统计整幅图的 Δ / I 分布如果直方图很宽说明这个场景的偏振度分布散不适合用单一 p 做全局减法。针对第三个问题工业上更稳的方案是直接上分焦平面偏振相机单帧同时输出 0°、45°、90°、135° 四个方向图运动场景也能用但这是硬件成本换稳定性后面代码部分我会给一个四方向 Stokes 重构的替代方案。3. 用偏振差分跑通水下增强最小可复现代码3.1 数据准备正交偏振图从哪来、怎么才算合格偏振差分需要一个前提两幅正交偏振图描述的是同一个场景。实现方式有两种。第一种是分焦平面偏振相机传感器每个 2×2 像元内置 0°、45°、90°、135° 四个方向的微偏振片单帧输出四个方向的图0° 与 90° 天然同帧同曝光适合水下机器人这类运动平台代价是空间分辨率下降微偏振片之间还有串扰DoLP 会被低估。第二种是普通相机加线偏振片先拍 0°再旋转 90° 拍第二张成本低但要求场景静止、光照稳定适合静水实验或水下三脚架拍摄水下运动场景基本拍不了。拿到两幅图后先做三件事确认两幅图都没有大面积过曝确认通道没有完全饱和确认两张图没有明显错位。偏振差分对数值一致性极其敏感所以图像保存建议用 PNG 或 TIFF 这类无损格式不要用 JPEG块边界伪影会在减法之后以网格纹理的形式放大。需要对齐时用相位相关做配准这一步放在算法之前具体代码在第 5 章排查部分给出。3.2 核心算法偏振差分重建的 Python 实现下面是一段可以直接跑的偏振增强流程。它不依赖任何深度学习框架只用到 NumPy 和 OpenCV。import numpy as np import cv2 def underwater_polar_enhance(im0, im90, p_scatNone, roiNone): # 输入两幅正交偏振图尽量用 ANYDEPTH 读入 16 位原始数据 im0 im0.astype(np.float32) im90 im90.astype(np.float32) # 总光强两正交分量相加等价于无偏振片时的原始强度 I im0 im90 # 偏振差分散射光的偏振分量主要集中在这里 Delta np.abs(im0 - im90) if p_scat is None: # 自动估计后向散射偏振度 if roi is None: # 默认取图像顶部中央 10% x 30% 区域通常是无目标的背景水体 h, w I.shape roi (slice(int(h * 0.05), int(h * 0.15)), slice(int(w * 0.35), int(w * 0.65))) # 该区域几乎没有目标光I 近似等于 DDelta 近似等于 p * D p_scat float(np.mean(Delta[roi]) / (np.mean(I[roi]) 1e-6)) p_scat float(np.clip(p_scat, 0.05, 0.95)) print(festimated p_scat {p_scat:.3f}) # 后向散射强度 偏振差分 / 偏振度 D Delta / max(p_scat, 1e-6) # 目标信号 总光强 - 后向散射 S I - D S np.clip(S, 0, None) # 用 2%~98% 分位数做线性拉伸避免单个高光像素拉坏整体对比度 lo, hi np.percentile(S, (2, 98)) S_display np.clip((S - lo) / (hi - lo 1e-6) * 255.0, 0, 255) return S_display.astype(np.uint8), p_scat逻辑说明I im0 im90这一步很多人会习惯性除以 2其实不除也可以因为后面 D 的估计和使用都基于同一个尺度最后归一化会消除整体缩放。如果这里除以 2D 的计算结果也会跟着缩放最终 S 只是整体变暗一个系数显示时又被拉伸回来纯属多此一举。Delta np.abs(im0 - im90)用的是绝对值差因为偏振方向定义不受正负影响重要的是强度的差异大小。参数说明p_scat传入 None 时自动从背景区域估计这个估计的前提是 ROI 内没有目标一旦框到鱼或水草p 会被显著抬高导致散射除不干净。roi参数可以手动传入自定义区域形式是(slice(rows), slice(cols))。p 的合理范围一般在 0.05 到 0.5 之间超过 0.5 要先怀疑区域选错了。最终输出用 2% 和 98% 分位数截断是为了防止远景中个别极端像素把动态范围撑破。3.3 跑通后先查中间量别等结果出来才后悔很多人跑完只看最终图觉得“好像变暗了”就急着调参。我会先打印几个中间量确认物理模型每一步都成立再决定调什么。def inspect_mid(im0, im90, p_scat): I im0 im90 Delta np.abs(im0 - im90) D Delta / max(p_scat, 1e-6) print(fp_scat: {p_scat:.3f}) for name, arr in [(I, I), (Delta, Delta), (D, D)]: print(f{name}: min{arr.min():.1f} max{arr.max():.1f} mean{arr.mean():.1f}) # 统计被减成负值后被截断的像素比例 black_ratio (D I).mean() print(fD I 的像素占比: {black_ratio * 100:.2f}%)逻辑说明这个函数的输出能直接告诉你三件事。第一Delta的均值如果不到I均值的 5%说明两幅偏振图差异微弱要么偏振片没转到位要么这个水体的散射光本身偏振度很低算法前提已经动摇。第二D的均值应该明显小于I的均值如果 D 和 I 接近说明后向散射被估计得过强几乎把整幅图都当成雾减了。第三black_ratio是减完之后被截断成纯黑的像素比例超过 10% 说明 p 取小了D 减过头需要把 p 调大接近 0 且雾感残留严重则是 p 取大了需要调小。这套检查只需要几行代码能省掉大量“盲调 p”的时间。4. 参数调优与场景边界p、彩色通道和曝光匹配4.1 p_scat 的自动估计与手动微调p_scat 是整个流程里最核心也最“玄学”的参数它代表后向散射光的偏振度。选错之后的表现很有规律p 偏小D Δ / p 偏大散射减过头画面整体偏暗、远景死黑、目标边缘出现黑边p 偏大D 偏小雾残留增强幅度弱图像只是被拉伸变亮但雾气依旧。所以调参本质上是在“减过头”和“没减够”之间找平衡。自动估计的代码在第 3 章已经给了核心逻辑是找一块没有目标的背景水体认为该处 I 近似等于 D然后 p Δ / I。这个估计的准确性完全取决于 ROI 选择常见的坑是 ROI 里混进了水草、鱼或者高光反射导致 p 偏高。我一般会在自动估计的基础上做一次人工校验跑完看中间量如果black_ratio在 5%~10% 之间把 p 调大 10%~20%如果雾感明显把 p 调小 10% 左右。一次调参幅度不要超过 20%调完看统计量而不是只看预览图。还有一点需要注意p 和“图像整体偏振度”不是一回事。很多人会拿整幅图的 Δ 均值除以 I 均值来估计 p这个值一定会偏大因为目标区域虽然退偏振但总有少量偏振差分混在里面。必须用背景区域或者用四方向 Stokes 重构后再估计才更接近真实的 D 偏振度。4.2 彩色图要不要分通道三通道偏振与白平衡灰度图跑通之后直接对灰度做增强会丢失色彩而且红色通道衰减严重增强完整体是青色的。更合理的做法是分别处理 RGB 三个通道因为不同波长的散射强度和偏振度都不同分通道后用各自估计的 p 去减能保留原本的颜色比例。代价是三个通道的 p 值差异可能很大减法之后会出现明显的颜色偏移尤其红色通道信号弱噪声被放大。我的处理流程是三通道分别调用同一套增强逻辑然后在输出端加一个灰世界白平衡把三个通道的均值拉到同一水平。代码片段如下def enhance_color(im0_color, im90_color): # 输入是两张 BGR 彩色偏振图逐通道做偏振增强 out np.empty_like(im0_color, dtypenp.uint8) for c in range(3): out[..., c], _ underwater_polar_enhance( im0_color[..., c].astype(np.float32), im90_color[..., c].astype(np.float32), p_scatNone) # 灰世界白平衡把三通道均值归一化到同一水平 means [out[..., c].mean() for c in range(3)] gray sum(means) / 3 for c in range(3): out[..., c] np.clip(out[..., c].astype(np.float32) * gray / (means[c] 1e-6), 0, 255) return out.astype(np.uint8)参数说明每个通道的 p 都由该通道的背景区域独立估计所以红色通道如果衰减太严重背景区域可能全是噪声估出来的 p 没有意义。这种情况下我做两个处理一是白平衡之前先把三通道的 p 做一次中值滤波避免单通道异常二是如果红色通道的均值小于蓝色通道的 5%直接放弃该通道增强用绿蓝通道合成结果然后用色彩迁移把红色通道补回来。注意白平衡不能恢复已经被水体完全吸收掉的红光信息。如果深水区红色通道值全是 0白平衡只是在放大噪声这时更要紧的是做颜色迁移或者通道合成而不是继续增强。4.3 曝光锁定与偏振取向误差数据采集阶段的命门偏振增强的结果上限有一半由数据采集决定。用旋转偏振片方式拍摄时两幅图必须做到三点同一场景、同一曝光、同一白平衡。相机一旦开了自动曝光两幅图的强度基准就变了后面的差分计算全部失真。我一般会建议先把相机切到手动模式锁定快门、光圈、ISO 和白平衡再连拍两张。偏振片旋转角度是另一个容易忽略的误差源。手工旋转很难精确到 90°如果两幅图的实际夹角是 80°Δ 会被低估估计出来的 p 偏高结果就是雾除不干净。手头如果有四方向偏振图0°、45°、90°、135°就不要只用 0°/90° 做差而是用 Stokes 参数重构能同时吸收角度误差def stokes_from_four(I0, I45, I90, I135): # 输入四个方向的偏振图输出总光强 S0 和线偏振度 DoLP S0 (I0 I45 I90 I135) / 2 S1 I0 - I90 S2 I45 - I135 DoLP np.sqrt(S1 ** 2 S2 ** 2) / (S0 1e-6) return S0, DoLP逻辑说明S0 是总光强S1 和 S2 是线偏振分量在两个正交基上的投影。用 S1 和 S2 的模重建差分可以避免只用 0°/90° 时因为偏振片取向偏差导致的能量泄漏。拿到 S0 和 DoLP 之后增强公式变为 S S0 − (S0·DoLP)/pp 仍然用背景区域的 DoLP 均值来估计。这个方法对角度的要求从“严格 90°”放宽到“四方向大致正确”鲁棒性明显提升。5. 水下偏振增强的四个翻车现场与排查清单5.1 现象恢复后远景出现深色斑块或黑边这是偏振增强最常见的翻车形态。处理完的图里远处水体被压成深蓝黑色目标轮廓边缘还挂着一圈黑边。原因是远景区域没有目标信号S 的真实值本来就接近 0当估计的 p 偏小时D 被算得过大I − D 变成负值后被截断成 0。边缘黑边则是因为目标边缘两侧的偏振度差异大同一 p 无法同时适配前景和背景。解决的办法有两个方向。第一是限制减法强度让每个像素最多减去总光强的 90%D_limited np.minimum(D, I * 0.9) S I - D_limited第二是对 D 做高斯模糊再减。后向散射在空间上是平滑渐变的而目标边缘通常是高频信号把 D 模糊后再做减法可以明显减少边缘伪影D_smooth cv2.GaussianBlur(D, (25, 25), 0) S I - D_smooth模糊核的大小要根据图像分辨率调整2000 像素宽以上的图用 25小图改成 11 左右。代价是细节区域的散射估计变钝对比度提升会略弱但在画面整体干净度上更划算。5.2 现象目标高光过曝暗部细节反而丢了增强之后白色物体、金属表面一片死白暗部的纹理反而更看不清。这通常不是减法的问题而是归一化的问题。偏振减法把动态范围拉开之后线性拉伸会把原先的高光推到 255暗部被压到更低。遇到这种情况先把归一化从 min-max 改成百分位截断np.percentile(S, (1, 99))比 (2, 98) 更保守。如果高光还是溢出可以对 S 做一条轻度的 gamma 压缩S_disp 255 · (S/255)^0.75相当于给高光区按下去了 1.3 档。暗部细节丢失严重时直接在 S 上跑 CLAHEclipLimit 取 2~3tileGridSize 取 8×8效果比全局拉伸更稳。我自己的习惯是保留两组输出一组是“物理重建结果”供算法评估和进一步处理一组是“增强显示结果”经过百分位截断和 CLAHE供人眼观察。后者更适合直接出图前者更适合继续做分割或目标识别。5.3 现象结果图出现条带/网格伪影增强结果里出现周期性条带或棋盘格纹理原因集中在两类。分焦平面偏振相机从四个方向图里抽取 0°/90° 子图时采样位置搞错就会产生棋盘格旋转偏振片方式拍两帧时场景有微小位移差分后目标边缘出现双重轮廓相位错位在高频区域会形成斜条纹理。分焦平面相机的像素排布问题需要靠厂家 SDK 的偏振去马赛克校正来解决手动重排很难完全消除。两帧微位移的问题可以用相位相关配准代码很轻量def align_phase(moving, reference): # 估计亚像素平移并做仿射校正 g_m cv2.cvtColor(moving, cv2.COLOR_BGR2GRAY).astype(np.float32) g_r cv2.cvtColor(reference, cv2.COLOR_BGR2GRAY).astype(np.float32) (dx, dy), _ cv2.phaseCorrelate(g_m, g_r) M np.float32([[1, 0, dx], [0, 1, dy]]) return cv2.warpAffine(moving, M, (reference.shape[1], reference.shape[0]))逻辑说明cv2.phaseCorrelate用相位相关计算两张灰度图之间的亚像素平移量返回的 (dx, dy) 直接构建仿射矩阵。配准之后如果还有轻微条带再配合 5.1 里的 D 高斯模糊基本上能把周期性伪影压到肉眼不可见。5.4 现象雾感还在对比度没有提升算法跑完只是色调变了雾还像一层纱一样罩在上面。这时候先打印两幅图的 Δ 均值和估计出的 p_scat。如果 Δ 均值不到 I 均值的 5%说明这个场景的后向散射偏振度本身很低偏振信息弱换曝光或换蓝绿波段拍摄可以缓解但偏振方法已经接近适用范围边缘。如果 p_scat 超过 0.5先怀疑 ROI 选到了目标重新框一块纯水区域再看。如果这些统计量都正常但雾感依旧问题出在 p 偏大散射没有被减干净。把 p 调小 10%~15% 再跑一次。还有一种容易被忽略的情况水体极度浑浊时多次散射会让光的偏振信息快速丢失偏振度和目标反射光一样趋近于零偏振差分不再有效。这种水体更适合用暗通道先验加深度估计的思路或者直接用基于学习的水下增强模型偏振方法不必硬上。6. 进阶验证用合成退化数据验证算法与定量指标判断一套偏振增强算法到底有没有把散射去掉最可靠的方式是合成数据。给定一张清晰的参考图 J构造深度梯度、透射率和后向散射再反推出两幅正交偏振图跑完算法和真值做对比所有误差都能算到像素级。import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity # J 为清晰灰度图0~255 uint8模拟从近到远的透射率梯度 h, w J.shape depth np.tile(np.linspace(0, 1, w)[None, :], (h, 1)) t np.exp(-0.6 * depth) # 透射率近处高远处低 D_scat 0.7 * (1 - t) # 后向散射光强 S_sig J / 255.0 * t # 衰减后的目标光 p_gt 0.25 # 设定后向散射偏振度 I0 np.clip((S_sig D_scat * (1 p_gt) / 2) * 255, 0, 255).astype(np.uint8) I90 np.clip((S_sig D_scat * (1 - p_gt) / 2) * 255, 0, 255).astype(np.uint8) enhanced, _ underwater_polar_enhance(I0.astype(np.float32), I90.astype(np.float32)) # 消除整体亮度缩放后再计算指标 e0 (enhanced - enhanced.min()) / (enhanced.max() - enhanced.min() 1e-6) j0 J / 255.0 print(PSNR:, peak_signal_noise_ratio(j0, e0)) print(SSIM:, structural_similarity(j0, e0, data_range1))真实水下图没有真值可以用三个替代指标做长期记录指标计算方式关注点PSNR / SSIM合成数据下与真值逐像素比较物理模型是否成立灰度方差S.std() / I.std()对比度是否真实提升信息熵熵值变化细节是否增加噪声是否被误当细节我最开始做偏振增强时习惯直接调 p调到一张图好看就觉得成功了换了场景立刻翻车。后来改成先打印 Δ 统计量、再跑合成数据验证模型、最后才在真实数据上微调稳定很多。建议你也把这个顺序固化到流程里。希望帮到你。本文还有配套的精品资源点击获取