news 2026/10/16 7:55:02

基于偏振物理模型的水下去雾:MATLAB完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于偏振物理模型的水下去雾:MATLAB完整实现

水下拍摄的片子到手后,第一眼往往不是蔚蓝,而是灰里透绿的奶白色。对比度低、颜色怪、细节像隔着一层磨砂玻璃,这和陆地上的雾霾图完全是两个概念。之前我试过直接用各种大气去雾算法套到水下画面上,结果经常越算越脏,要么边缘发黑,要么背景过曝。后来我把思路整个换掉,改成基于偏振物理模型的水下图像去雾,用MATLAB从零实现一版可复跑的代码,效果立刻稳了不少。

这个项目做的事情很简单:在镜头前加一块可旋转的偏振片,分别拍摄0度和90度两张图像,利用后向散射光与目标反射光在偏振特性上的差异,把“雾”这一层从图像里拆出去。它适合正在做水下成像、海洋工程、机器视觉相关课题的同学参考,也适合对图像复原感兴趣、想换一种物理先验思路的开发者。整个链路不长,但每一步都有值得说道的细节。

下面我按自己的实操顺序,把思路、公式、MATLAB代码和踩过的坑完整记录下来。

1. 项目思路拆解:为什么水下去雾要转偏振物理

1.1 水下“雾”和空气雾的本质差别

水下图像发白,根本原因是水体里的悬浮颗粒把入射光散射了。这部分散射光混杂在目标反射光里,被镜头一并接收,等效于给原始场景叠加了一层强度很高的环境光。水面之下天然存在的吸收效应还会让红光迅速衰减,所以画面整体偏蓝绿。

这和陆地雾霾不完全一样。空气中的雾主要是大气溶胶的后向散射,模型相对单纯,经典的暗通道先验就能解得很好。水下却有一个额外的麻烦:前向散射对细节的模糊和后向散射对对比度的压制叠加在一起,而且水体对不同波长的衰减差异极大,暗通道先验里“暗通道趋近零”的假设在水下经常不成立,尤其是大面积偏蓝的区域,暗通道值并不低,导致透射率被严重高估。

偏振方法的出发点不是图像灰度的统计规律,而是散射过程本身的物理性质。散射光在散射过程中会获得相当强的偏振状态,目标反射光却大多是退偏的。这个差异让“拆雾”变成一个可测光学量的问题,而不是纯靠猜。

1.2 偏振物理模型的直觉

做偏振去雾,一句话解释就是:后向散射光是部分偏振的,目标反射光基本是非偏振的,于是旋转镜头前的偏振片会让散射光的强度发生变化,而目标光的强度几乎不变。把两张正交偏振图像相减,差值主要来自散射光;把它加到总强度里扣除,就能得到不含散射的目标信号。

实际现场操作时,你会看到非常直观的现象:旋转偏振片,浑浊水体背景的亮度在明显闪烁,水底的石头、鱼、管道这类物体的亮度却变化很小。这种肉眼可见的差异,正是整个模型的物理基础。我记得第一次在水池实验时,看到背景光跟着偏振片转动而变暗变亮,顿时觉得这个方案是靠谱的——它不依赖特定场景的颜色分布,而是直接从光波振动方向上做物理分离。

还要多提一句:水中颗粒通常是随机取向、尺度不一,多次散射之后光的偏振态会变得比较温和,不会像镜面反射那样完全线偏振。所以模型里处理的不是一个“完全偏振”信号,而是一个“部分偏振”信号,它的偏振度也就是核心参数p。

1.3 关键假设与适用边界

偏振模型有一个最理想化的假设:目标反射光完全非偏振。实际水下场景中,鱼鳞、金属管道、光滑岩石等表面可能会产生强偏振反射,这部分会被算法误当成散射光给扣掉,造成目标变暗或出现伪影。处理这类场景时需要额外的偏振方向筛选或遮罩,不能无脑套公式。

另一个前提是两次拍摄之间相机和场景不能大幅移动。手动旋转偏振片时,相机本身不能抖,否则像素错位会直接影响差分结果。现实拍摄场景里,更稳妥的做法是使用支持偏振采集的工业相机,或者后期做一个刚性配准。

这套方法的定位是:在浑浊度中等、场景以漫反射为主的水下环境中,用最少的硬件和参数获得大幅提升的清晰度。它不追求一键万能,而是提供一个物理上可解释、可调试的去雾基线。

2. 核心原理与关键参数估计

2.1 成像模型与最终公式链

先给出水下偏振成像的简化模型。相机获取的总强度I由目标直接透射光D和后向散射光B叠加:

I = D + B

其中D = J·t,J是未衰减的目标光强,t是透射率。后向散射光B可写成:

B = A·(1 - t)

A被称为全局背景光强度,表示光路无穷远处的散射光。水下成像时,水和悬浮颗粒把光一路散射又一路吸收,这套衰减-叠加关系大致成立。

再引入偏振维度。设后向散射光的偏振度为p,目标光近似非偏振,那么透过两个正交方向的偏振片后:

I_parallel = J/2 + B_parallel
I_perpendicular = J/2 + B_perpendicular

其中B_parallel - B_perpendicular = p·B。两式相减:

ΔI = I_parallel - I_perpendicular = p·B

所以后向散射光的强度可以直接从两张正交偏振图像的差分中恢复:

B = ΔI / p

接着利用B = A·(1 - t),透射率为:

t = 1 - B / A

最后目标光:

J = (I - B) / t

这条公式链是整个项目的骨架,后面所有MATLAB代码都是在实现它。关键是:只要准确估计出A和p,整个去雾过程不需要任何手工调参,也不需要机器学习训练。

2.2 全局背景光A的估计方法

估计A不能随便挑一个亮斑。如果取到景物的高光区,会把目标光误当背景,导致整张图偏暗或产生怪异色斑。

我用的方法是参考暗通道先验里“最亮像素”的思路:先计算灰度总强度图,取亮度最高的前0.1%像素,再把这些像素附近邻域内的平均强度作为A。这个做法的前提是画面中存在一片没有目标的“纯水体”区域,那里的t接近0,像素强度几乎全是后向散射光,也就是A本身。亮度最高的地方通常就是这类区域,前提是画面没有过曝。

如果镜头里有强烈的阳光耀斑,或者靠近水面有极亮的高光,自动估计就会翻车。所以我通常在代码里预备一个手动选择模式的接口,通过ROI框选一片明显的背景水体,用该区域的均值替代自动估计。现场有个经验:框选时尽量选距离相机中等偏远的纯水体,不要选最近的强散射区,因为近处水体散射未必饱和到A的渐近值。

2.3 后向散射偏振度p的估计

p是偏振模型里最敏感的参数。它定义为背景散射光中偏振部分占全偏振光强的比例,受颗粒尺度、浓度、入射角度影响。

最简单的估计方式是直接在背景区域上计算:

p = mean(ΔI_background) / mean(I_background)

注意要用“背景区域”而非全图。我这边的实测经验是,p的估计对结果影响极大,直接决定去雾的力度:p估计偏大,B被低估,去雾不彻底;p估计偏小,B被放大,容易出现负值或大面积发紫。

因此代码里要加两层保护:一是p限制在一个物理合理区间,比如0.05~0.85;二是用像素占比更大的背景mask做中位数比值,而不是用均值。中位数的抗噪性好,不容易被个把过曝点带跑。

2.4 为什么不能用普通图像去雾代替

很多人会问:水下直接用暗通道先验不行吗?不行。水下环境里“暗通道接近0”的假设经常失效,透射率估计的误差会导致去雾后大面积偏红或者偏紫。偏振方法好在多了一个物理维度,把散射光强度直接测出来,而不是通过统计去猜。

不过两者并不冲突。实际项目里我经常把偏振去雾的结果作为中间图,再叠一个轻量级的暗通道修正或白平衡,效果比只用一个强很多。后面代码里我也会加这一层后处理。

3. MATLAB完整实现流程

3.1 数据准备:两张正交偏振图像的获取

要复现这个项目,硬件层面的准备比较轻量:一个可以旋转的线性偏振片,一台普通相机,再加一个稳定的拍摄平台。拍摄时先记录偏振片透光轴方向为0度的一张,再快速旋转到90度拍第二张。需要注意三点:

  • 两次曝光参数必须一致,ISO、快门、光圈都不能动,否则差分结果里混入曝光差异;
  • 相机固定住,场景里的移动物体要避开;
  • 最好用RAW格式保存。JPEG的压缩块会让局部偏振差失真,恢复后容易出现马赛克纹理。

如果手里有那种微偏振阵列的工业相机,一次曝光就能得到四个方向的偏振图像,那就更方便。我们这里用的仍是“两张正交图”的最小输入。

MATLAB里读取图像后,先统一转成double:

I0 = im2double(imread('pol0.jpg')); I90 = im2double(imread('pol90.jpg'));

如果两张图像存在轻微错位,先做一个刚性配准:

[optimizer, metric] = imregconfig('monomodal'); I90 = imregister(I90, I0, 'rigid', optimizer, metric);

这一步很重要,尤其是手动偏振片加手持相机的场景。配准后再进入主算法。

3.2 主流程代码与逐步解释

下面给出核心函数,输入是两张正交偏振图像,输出是去雾结果J和中间量结构体aux。

function [J, aux] = uwPolarDehaze(I0, I90, opts) % 基于偏振物理模型的水下图像去雾 % 输入: % I0 - 偏振片0度方向图像 % I90 - 偏振片90度方向图像 % opts - 可选参数结构体 % 输出: % J - 去雾复原图像 % aux - 中间量: p, A, B, t if nargin < 3, opts = struct(); end if ~isfield(opts, 'pMin'), opts.pMin = 0.05; end if ~isfield(opts, 'pMax'), opts.pMax = 0.85; end if ~isfield(opts, 'tMin'), opts.tMin = 0.10; end if ~isfield(opts, 'bgMode'), opts.bgMode = 'auto'; end % 1. 总强度与偏振差 Itotal = I0 + I90; Delta = abs(I0 - I90); % 2. 估计全局背景光 A A = estimateAinf(Itotal, opts); % 3. 估计后向散射偏振度 p p = estimateP(Itotal, Delta, A, opts); % 4. 后向散射分量 B = Delta ./ p; B = min(B, Itotal * 0.95); % 物理约束: B 不能超过 I % 5. 透射率 t = 1 - B ./ A; t = max(t, opts.tMin); % 6. 复原 J = (Itotal - B) ./ t; J = min(max(J, 0), 1); % 输出中间量,方便排查 aux.p = p; aux.A = A; aux.B = B; aux.t = t; end

这里有个小地方要解释:为什么B要限制在Itotal的95%?因为B=ΔI/p在个别强偏振差像素上可能计算得比总强度还大,这在物理上是不可能的,Etot应该包含目标光和散射光的总和。限制B之后可以避免透射率出现负数。

3.3 背景光A与偏振度p的子函数实现

下面两个子函数是核心参数估计的实现。

function A = estimateAinf(Itotal, opts) % 估计全局背景光: 取最亮像素邻域的均值 gray = rgb2gray(Itotal); topN = max(1, round(numel(gray) * 0.001)); [~, idx] = sort(gray(:), 'descend'); brightIdx = idx(1:topN); [rows, cols] = ind2sub(size(gray), brightIdx); % 扩大邻域, 降低单个过曝点的影响 mask = false(size(gray)); win = 5; for k = 1:numel(rows) r0 = max(1, rows(k)-win); r1 = min(size(gray,1), rows(k)+win); c0 = max(1, cols(k)-win); c1 = min(size(gray,2), cols(k)+win); mask(r0:r1, c0:c1) = true; end A = zeros(1, 1, 3); for ch = 1:3 tmp = Itotal(:,:,ch); A(1,1,ch) = mean(tmp(mask)); end end

手动模式可以这样改:用imrect拉一个矩形框,然后取框内均值。我在野外实操时,只要环境光不均匀,或者画面里有强光源直射,都直接切到手动模式,稳定很多。

function p = estimateP(Itotal, Delta, A, opts) % 估计后向散射光偏振度 p A_scalar = max(A(:)); % 以最强通道为参考 gray = rgb2gray(Itotal); deltaGray = rgb2gray(Delta); % 背景mask: 亮度接近A的高亮区域 bgMask = gray > A_scalar * 0.85; if nnz(bgMask) < 5000 % 若高亮区域太少, 放宽阈值 bgMask = gray > A_scalar * 0.7; end % 用中位数比值, 抗噪 p = median(deltaGray(bgMask)) / median(gray(bgMask)); p = min(max(p, opts.pMin), opts.pMax); end

p的中位数估计值得多说一句。我最初用的是均值,某次实验背景区域刚好有一个移动的白色漂浮物,把ΔI均值拉得很高,结果p偏大,去雾力度不够,背景还是白蒙蒙的。换成中位数之后,这类离群点的影响被压下去了,p值稳定很多。

3.4 后处理:白平衡与局部对比度增强

直接输出去雾图往往偏暗,而且因为偏振差分的关系,图像会带有淡紫色或淡绿色残留。我习惯再加三步:

  1. 灰度世界白平衡,纠正偏色;
  2. 整体亮度线性矫正;
  3. 对亮度图像做CLAHE局部对比度增强,把细节“顶”出来。
function Jout = postProcess(Jin) % 水下偏振去雾结果的后处理 Jout = Jin; % 灰度世界白平衡 for ch = 1:3 Jout(:,:,ch) = Jin(:,:,ch) / mean2(Jin(:,:,ch)); end % 全局亮度拉伸: 防止偏振片带来的整体变暗 Jout = Jout / max(Jout(:)) * 0.95; % 局部对比度增强: 只作用于亮度分量 JLab = rgb2lab(Jout); JLab(:,:,1) = adapthisteq(JLab(:,:,1), 'NumTiles', [8 8], 'ClipLimit', 0.02); Jout = lab2rgb(JLab); % 确保输出范围 Jout = min(max(Jout, 0), 1); end

白平衡这一步在主观观感上几乎是决定性的。偏振模型本身不做色彩校正,如果场景里蓝光特别强,直接输出的图仍然偏蓝。灰度世界假设虽然粗糙,但对绝大多数水下场景都有正向效果。如果你想要更精细,可以在白平衡前先做一次红通道衰减补偿,不过那就离本项目的主题远了。

3.5 核心参数速查表

参数含义经验值调整方向
pMin偏振度下限0.05去雾过猛时调大
pMax偏振度上限0.85去雾不足时调小上限
tMin透射率下限0.10远处噪声大就调大,想更透就调小
bgMode背景估计模式auto / manual场景有强光时切manual
win背景邻域半径5有孤立亮点时调大

参数调整没有万能公式,我的经验是优先固定p的边界,再调tMin。tMin这个值对画面影响非常直观:取值太小时远处区域噪声被放大,背景颗粒感严重;取值太大时远处散射光删不掉,恢复图像远中近过渡不自然。自己写代码时,建议做一个交互式滑动条,实时看tMin变化,比一次次跑脚本高效得多。

4. 常见问题排查与避坑实录

4.1 偏振度p估计不稳怎么办

p估计不稳的最常见症状是:去雾后的图像某些区域出现明显发紫或发暗的大片色块。原因基本是背景mask选得不干净,把强偏振的目标反射区当成背景区。

我遇到过一次很典型的情况:场景底部有一块覆满油膜的光滑金属板,反射偏振度远远高于背景散射光。自动估计出来的p值明显偏小,导致B被放大,那一片区域直接过曝成紫红色。后来处理方式是在估计p之前先计算像素梯度,剔除梯度剧烈变化的区域,只保留平坦的浓雾背景。

gradMag = imgradient(rgb2gray(Itotal), 'sobel'); bgMask = bgMask & (gradMag < 0.05);

这个过滤条件简单有效,因为真正的背景水体区域纹理非常弱,梯度小。如果再配上中位数估计,基本能稳住p。

4.2 远处区域噪声放大严重

偏振去雾最后一步是除以透射率t,远处物体t本来就小,一除就把噪声放大。这是物理模型方法绕不开的问题,也是很多初版实现观感变差的主因。

我的处理策略是三层防线:

第一层是tMin截断,把t限制在0.1以上。第二层是对t做一次轻度平滑,避免透射图出现单像素深坑:

t = imgaussfilt(t, 2);

第三层是在输出前对结果做一次3x3中值滤波或者去噪,重点压制暗区的颗粒感。如果场景本身噪声很大,建议在拍摄阶段就压低ISO,或者多帧平均后再进入算法。

这里有一个设计权衡:t平滑过头会让远处边缘产生光晕,目标轮廓像蒙了一层纱。轻度平滑2到3个像素一般不会出问题,但不要用大核。

4.3 偏色和白色区域过曝

偏振差分恢复后,最常见的偏色是整体偏紫、偏红,这是后向散射光照波长依赖导致的。水对不同波段散射能力不同,蓝绿光散射更强,红光衰减更快,简单地把ΔI均分给RGB三个通道,本质上已经引入了一个与场景无关的色差。

所以后处理里的白平衡不是可选项,是建议必选项。如果自动白平衡后还是偏色,可以手动调RGB增益,或者换用带衰减补偿的水下成像模型。实际项目里,我在白平衡前会额外乘一个红色增益系数,经验值在1.2到1.8之间,具体取决于水的浑浊度。

白色区域过曝的问题是另一个极端:目标反射太强,I0或I90中的某一张直接饱和,ΔI失真,B被高估,恢复结果出现奇异色。解决思路只有一个——拍摄时检查直方图,确保两张偏振图像都没有大面积过曝。如果没有RAW格式,甚至可以考虑略微欠曝一点拍摄,偏振差分算法对暗部和噪声的容忍度比高光过曝好得多。

4.4 偏振片角度不精确导致结果不稳定

手动旋转偏振片很难精确转到0度和90度。角度偏差会使得计算出的ΔI小于真实值,B被低估,去雾力度下降。我自己做过一个简单实验:在桌面上把圆形偏振片放在刻度量角器上拍摄,分别用0/90、10/100、20/110三组角度跑了一遍算法,视觉差异在浑浊度高的场景里很明显,角度偏得越多,背景残留越重。

要修正这个问题,有两个方向:

一个方向是硬件层:使用工业偏振相机或用步进电机驱动偏振片,确保角度精度。另一个方向是算法层:在参数估计中增加一个角度修正项,但代码复杂度会上升不少,一般情况下不划算。

对大多数课题验证场景,手动偏振片加一个三角架,配合一个纸质量角器就能获得可接受的结果。角度偏差在5度以内时,视觉影响很小。

4.5 验证用合成数据怎么做

没有水槽和偏振片时,可以用MATLAB合成数据来验证算法链路。我先构造一个真实场景,加一层已知强度的散射光,再给它分配一个已知偏振度,最后合成两张正交偏振图像。这样能验证代码参数估计和恢复公式是否自洽。

rng(0); Jtrue = rand(256, 256, 3); A = 0.8; p = 0.6; t = linspace(0.1, 1, 256)' .* ones(1, 256); B = A .* (1 - t); I = Jtrue .* t + B; Delta = p .* B; I0 = I / 2 + Delta / 2; I90 = I / 2 - Delta / 2; [Jrec, aux] = uwPolarDehaze(I0, I90); psnrVal = psnr(Jrec, Jtrue); disp(['恢复PSNR: ', num2str(psnrVal)]);

合成数据里因为前提假设完全成立,PSNR一般很高。注意看如果PSNR不理想,先检查你写入代码的模型是否和公式链一致,尤其是B=ΔI/p这一步有没有写反。

这个验证方式我非常推荐:它把算法链路和现实误差解耦,可以快速定位问题到底出在代码还是出在图像获取。我在项目里每次改参数估计逻辑,都会先跑一遍合成数据回归测试,再跑真实数据。

写在后面的个人经验

这个项目最让我感慨的一点是:一个看起来简单到只有三行核心公式的方法,真正在现场跑起来时,问题几乎全出在参数估计和数据质量上。偏振模型提供了一个极其优雅的线性拆解思路,但它的上限取决于你喂给它的两张图有多干净。偏振片旋转角度的准确性、背景区域的挑选、曝光一致性的控制,每一样都可能让结果从惊艳变成没法看。

后来我又试过把偏振去雾的输出作为前置模块,再接入深度学习的增强网络,整体鲁棒性比单独跑任一方法都好。如果你手头有其他后处理工具,不妨把这里的输出当中间特征用。这个思路或许可以成为你下一步扩展的方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 7:54:28

TuShare金融数据接口实战:从安装配置到回测清洗全链路指南

简介&#xff1a;本资源是面向Python金融数据分析初学者与量化实践者的TuShare开源工具完整学习包&#xff0c;聚焦中国股票历史数据获取、清洗与存储全流程&#xff0c;解决金融数据接口调用门槛高、文档分散、环境配置复杂等实际问题。压缩包共118个文件&#xff0c;涵盖67个…

作者头像 李华
网站建设 2026/10/16 7:53:36

Spring AI Alibaba停更后,Java开发者如何自建大模型适配层

1. 一个框架停更引发的技术选型焦虑前几天在技术群里看到有人转了一张截图&#xff0c;说 Spring AI Alibaba 这个项目停更了&#xff0c;紧接着就有人跟了一句“Java 在 AI 这条路上是不是彻底没戏了”。群里瞬间炸锅&#xff0c;有人开始盘点自己项目里引了这个依赖的地方&am…

作者头像 李华
网站建设 2026/10/16 7:52:44

dq坐标系下虚拟同步机构网变流器的核心优势与工程实现

前阵子在现场调一台构网型样机&#xff0c;旁边有位刚入行的同事问我&#xff1a;既然下垂控制也能让变流器参与电网调频调压&#xff0c;为什么大家都在讲虚拟同步机&#xff1f;这个问题其实问到了点子上。下垂控制解决的是“有差调节”的问题&#xff0c;但电网真正缺的是“…

作者头像 李华
网站建设 2026/10/16 7:51:42

AI原生超级全栈开发:从写代码到编排AI的实战指南

1. 从“写代码的人”到“编排AI的人”&#xff0c;这个训练营到底在教什么“AI原生超级全栈开发工程师训练营”这个名字&#xff0c;第一次看到的时候我愣了一下。全栈我懂&#xff0c;超级全栈是什么&#xff1f;AI原生又是什么&#xff1f;后来花了两周时间把市面上能找到的相…

作者头像 李华
网站建设 2026/10/16 7:51:28

上下文工程实战:从提示词到信息流,提升AI应用效果

1. 上下文工程到底在解决什么问题第一次听到“上下文工程”这个词&#xff0c;很多人会下意识觉得它只是“提示词工程”换了个马甲。但真上手做过几个AI应用之后就会发现&#xff0c;这两件事的侧重点完全不同。提示词工程关心的是“怎么把话说清楚”&#xff0c;而上下文工程关…

作者头像 李华
网站建设 2026/10/16 7:51:14

PCA与Autoencoder降维算法对比:原理、实战选型与工程坑位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华