☰
水下图像增强实战:波长补偿与暗通道去雾的Matlab实现
2026/10/20 16:26:44 网站建设 项目流程

水下图像增强这个话题,国内做的人其实不少,但大多数论文复现起来都挺折腾的。我接触这个方向主要是因为手头有个水下视频分析项目,拍回来的素材普遍发蓝发绿,远处一片模糊,拿去跑通用目标检测算法效果惨不忍睹。后来系统梳理了基于波长补偿加去雾思路的增强算法,用 Matlab 完整实现了一遍,整个流程跑通之后明显感觉对后续的分析任务帮助很大。这篇文章就把整个研究和实现过程拆开来讲,从物理模型到代码细节都过一遍,分享给同样被水下图像质量困扰的朋友。

水下图像为什么总是“自带滤镜”:不同退化原因要分开治

先说一个很多人容易忽略的点:水下图像画质差,不是单靠一个“锐化”或者“拉对比度”就能解决的,因为退化来自两个完全不同的物理过程。把这两个过程搞清楚了,后续选算法才不会跑偏。

1.1 吸收主导的色偏:为什么红光总是先消失

可见光在水中的衰减随波长变化非常明显。红光波长最长,能量低,在水里走不远就被吸收掉了;蓝光和绿光波长较短,穿透能力强,所以在水下图像里我们看到的往往是蓝绿色调的大片区域。我做个很简单的类比:这就好比隔着好几层蓝色玻璃纸看一张彩色照片,最先看不清的是红色细节,最后剩下来的只剩蓝绿信息。

从测量数据来看,在非常清澈的水中,红光大约在几米深就已经衰减得很厉害,绿光和蓝光可以穿透到更深的深度。这种选择性衰减直接导致一个现象:水下相机拍到的图像,红通道像素值普遍偏低,而蓝绿通道相对偏高。如果只是简单提亮整张图,蓝绿通道也跟着变亮,结果整张图仍然偏色,只是从暗蓝绿变成了亮蓝绿,本质问题没解决。

1.2 散射主导的模糊:后向散射把对比度吃掉了

水里面不是纯净的,有悬浮颗粒、浮游生物、有机物碎屑。光线在这些颗粒上发生散射,一部分散射光进入相机镜头,形成类似雾的“面纱”,专业上叫后向散射。这层光帘会让远景对比度急剧下降,细节糊成一片,色调也发白。

观察一下就能发现,水下图片远处的部分通常泛白泛灰,近处的东西相对清楚,这正是后向散射随距离累积的体现。用成像模型来描述,就是经典的 Jaffe-McGlamery 水下成像模型:相机接收到的光强等于直接反射光加上前向散射再加上后向散射。工程上做增强,通常不会把前向散射单独建模(太复杂而且收益有限),主流做法是把后向散射等价成大气散射模型里的“大气光”来处理。

所以结论就很清晰了:色偏问题靠波长补偿去解决,模糊问题靠去雾框架去解决。两条线分头处理,最后合并输出,这是目前这类算法的主流结构。

波长补偿的两种落地思路:从统计补偿到物理模型的取舍

波长补偿的核心目标只有一个:把因为水吸收导致偏低的红通道信息恢复出来,同时抑制蓝绿通道的过度增益。实际操作中,这个环节做得好的话,颜色立刻就会自然很多。

2.1 基于灰度世界的快速补偿法

灰度世界假设认为,自然场景中 RGB 三通道的像素均值应该趋于一致。水下图像因为红光衰减,红通道均值明显低于蓝绿通道,所以可以通过调整三通道的增益,让它们的均值重新拉到同一水平。

Matlab 里实现这个思路非常直接,核心逻辑就这么几步:

function imgOut = wbCompensate(imgIn) % 分通道统计均值 R = double(imgIn(:,:,1)); G = double(imgIn(:,:,2)); B = double(imgIn(:,:,3)); meanR = mean(R(:)); meanG = mean(G(:)); meanB = mean(B(:)); % 以绿通道均值为基准做增益修正 gainR = meanG / (meanR + eps); gainB = meanG / (meanB + eps); R2 = R * gainR; G2 = G; B2 = B * gainB; % 防止溢出,做一次裁剪和归一化 imgOut = cat(3, R2, G2, B2); imgOut = uint8(max(min(imgOut, 255), 0)); end

这个方法速度快,几乎是即时出结果,在很多视频流实时处理的场景里很吃香。但注意一个限制:灰度世界假设天然默认“整幅图像里的色彩均值是中性的”,如果画面里大面积是黄色沙地或者绿色水草,这个假设就会失真,补偿出来的颜色会发红或者发紫。

2.2 基于波长衰减系数的物理模型补偿法

更讲究一点的做法,是直接从波长衰减系数出发做补偿。水下每个通道的衰减系数不一样,文献里通常给出了不同水质下的典型衰减系数,大致结构是:

红通道衰减系数 > 绿通道衰减系数 > 蓝通道衰减系数

在 Matlab 里可以预先定义一组衰减系数,然后对每个通道做深度相关的补偿:

% 设定不同水质的衰减系数(示例值) % betaR = 0.8, betaG = 0.3, betaB = 0.2 这类数量级需要根据实际场景微调 betaR = 0.7; betaG = 0.3; betaB = 0.2; depth = 5; % 估计的平均有效深度 R2 = R .* exp(betaR * depth); G2 = G .* exp(betaG * depth); B2 = B .* exp(betaB * depth);

这里需要权衡的是“深度这个参数怎么估”。如果你连相机参数都不知道,那 depth 实际上就是一个可调参数,调大了红色就会过量,图像偏红;调小了补偿不足,图还是偏绿。我的建议是:如果没有深度传感器,那就用“场景统计补偿为主,物理模型系数作为先验约束”的方式,避免引入太多不确定参数。

2.3 我自己实验后推荐的组合策略

我实际跑下来的效果是:纯灰度世界补偿容易把本来正常的灰蓝色海水区域搞成明显的红色伪影;纯物理补偿又容易欠调。后来我改成了分段的混合策略——先计算红蓝通道相对绿通道的均值差异,再根据差异大小动态决定补偿强度,差异越大补偿越保守,差异小就正常补偿。这样既保留了计算简单的优点,又减少了极端场景下的偏色风险。

暗通道先验去雾算法的水下适配:公式改造与参数选择

去雾老玩家都应该知道何恺明提出的暗通道先验,这套理论在空气环境里效果非常好。但是水下直接用,效果往往很怪——图像去完雾之后反而发灰发暗。原因就在于水下场景的物理特性和大气场景有差异,需要对模型做改造。

3.1 标准暗通道先验的核心公式回顾

大气散射模型是:

I(x) = J(x) * t(x) + A * (1 - t(x))

其中 I 是观测图像,J 是待恢复的清晰图像,t 是透射率,A 是大气光(水下场景里就是背景光)。暗通道先验建立在“无雾清晰图像的局部区域,至少有一个通道的像素值趋近于 0”这一统计规律上。

先计算暗通道图:

function dark = calcDarkChannel(img, patchSize) % 对每个像素取邻域内RGB三个通道的最小值 % 实际中常用 minfilter 实现 h = fspecial('average', patchSize); darkR = imfilter(img(:,:,1), h, 'replicate'); darkG = imfilter(img(:,:,2), h, 'replicate'); darkB = imfilter(img(:,:,3), h, 'replicate'); dark = min(min(darkR, darkG), darkB); end

严格来说标准暗通道用“最小值滤波 + 三通道求最小”,上面这个近似版本用均值滤波替代了最小值滤波,速度更快但块状伪影更少,实际效果还挺不错,我在工程上是优先用均值平滑替代的。

然后估计透射率:

t(x) = 1 - omega * dark_channel(I(x) / A)

这里 omega 一般取 0.95,保留一点雾感,防止图像过度增强。A 的估计方式是取暗通道图像中最亮的前 0.1% 像素位置,在原图里找这些位置的像素最大值作为大气光值。

3.2 水下场景的核心修正点:透射率下限与色彩补偿

水下场景和大气场景最大的区别在于:水下即使是很近的物体,透射率也不会接近 1,因为水本身就有吸收。如果按标准公式直接算,近处物体会被过度去雾,出现灰蓝色不自然过渡;远处物体的透射率又会掉得非常快,产生色块断层。改造方法是给透射率加一个下限约束:

tMin = 0.4; % 水下场景常用下限值,比空气里的0.1要高不少 t = max(t, tMin);

这个 0.4 的值不是拍脑袋定的。我对比过 0.2、0.3、0.4、0.5 几组参数,0.2 会让远处的物体变得很暗很灰,0.5 会让增强效果不明显,0.4 在多数近海场景图里平衡性最好。当然如果是浑浊水域,这个下限可能还要往上提。

另外,标准暗通道先验中估计的大气光 A 在水下有系统性偏差。因为蓝绿通道本来就强,直接在原图上取最大值很容易把 A 估到高亮的蓝色区域,导致去雾后的颜色漂移。我用的方案是先把图像做一次初步波长补偿,再在这个补偿后的图像上估计 A,这样 A 的通道间关系更接近真实场景光。

还有一点容易踩坑:恢复出来的 J 在红色通道经常出现过饱和或暗部异常,所以去雾之后需要再补一次通道直方图调整,把红通道的动态范围拉到合理区间。

Matlab 工程实现全流程拆解:从单张图到批量处理

整个流程拆开之后,在 Matlab 里实现并不复杂,关键是模块划分清楚,后期调参方便。我最终整理的几个核心模块可以单独替换和复用。

4.1 整体流程的模块设计与调用结构

处理顺序是:输入原始水下图像 → 波长补偿得到颜色校正图 → 颜色校正图上做暗通道透射率估计 → 用原始图+透射率+大气光完成去雾恢复 → 可选的颜色直方图调整 → 融合输出。

我把这个流程封装成了几个独立函数,调用起来像流水线一样干净,在某个目标检测项目里直接把这套增强流程作为预处理环节,效果提升明显。主脚本大致长这样:

function enhancedImg = underwaterEnhance(imgPath, params) % 读取原始图像 I = imread(imgPath); % 第一步:波长补偿 Iwb = wbCompensate(I); % 第二步:在补偿图上估计大气光与暗通道 dark = calcDarkChannel(Iwb, params.patchSize); A = estimateAtmosphericLight(Iwb, dark); % 第三步:估计透射率并加下限约束 t = estimateTransmission(Iwb, A, params.omega); t = max(t, params.tMin); % 第四步:恢复无散射图像 J = recoverScene(Iwb, A, t); % 第五步:颜色直方图微调 J = colorAdjust(J, params.alpha); enhancedImg = J; end

第五步的颜色直方图微调我采用的最简做法是对 RGB 三通道分别做 1% 的裁剪线性拉伸。注意不是全局直方图均衡化,全局直方图均衡化在水下图片上非常容易把噪声一并放大,出现奇怪的颜色斑点。

4.2 大气光估计“95% 分位优先”的策略

标准暗通道用暗通道中最亮前 0.1% 像素在原图中的最亮值作为大气光,这个方法在纯净环境下很稳,水下有时候会因为高亮噪声点、人工光源或者水面反射导致 A 估得过大。我在工程里加了防抖逻辑:先算出暗通道图的 95% 分位值作为候选阈值,然后只在暗通道值大于该阈值的区域里搜索原图亮度最大值。这样如果画面里有一小块很亮的光斑,不会被直接当成大气光影响整体去雾量。

4.3 推荐参数一览与作用说明

把经验参数列个表格,方便直接对照用:

参数推荐初始值作用说明调参方向
patchSize(暗通道邻域)15×15控制暗通道图的粗糙度越大越粗糙但抗噪,越小细节越多但容易黑斑
omega(去雾保留系数)0.95控制去雾强度想更清晰就调大,想保留远近层次就调小
tMin(透射率下限)0.4防止远区过暗浑浊水调大,清水中可略调小
betaR 等衰减系数0.7 / 0.3 / 0.2物理补偿的强度主要用于生成参考图,统计补偿为主时可不调
alpha(颜色微调尺度)0.01归一化裁剪比例偏色严重时调大,图自然则保持小值

每个项目的水质情况不一样,这些参数需要实际调整。建议固定一个验证集(5 到 10 张代表性图片),每次改参数跑一遍,肉眼加指标双确认。

实验效果对比:主观质量与客观指标两条线都要看

研究做下来,只靠“看着舒服”是不够的,还是要落到量化指标上。我这边主观和客观各做了一套评价方案。

5.1 主观对比:用一组同一场景不同方法处理的图看差异

我自己整理了一组测试图,包括绿藻较多的浅水区、灰蒙蒙的近岸浑水、深水区蓝调很重的画面。对比结果大致是:直接灰度世界补偿后的图像颜色鲜艳,但红通道有时过强,偏橙红色;只用标准暗通道去雾的图像,色彩还是蓝绿色,只是对比度变好;而波长补偿+去雾组合处理后,色彩自然得多,远处细节和近处物体都能兼顾。

这里有个关键观察:单纯去雾虽然能提升对比度,但解决不了色偏;单纯颜色补偿提升不了远处模糊。很多论文强调二者缺一不可,是有道理的。

5.2 客观指标:参考无参考指标均可用

在水下增强领域没有标准真值图,所以无参考评价指标用得最多,有参考指标一般在仿真数据集上顺便验证。我会同时算这几个:

  • UIQM(水下图像质量指标):越高越好,综合色彩、清晰度、对比度,看色彩增强效果最直接;
  • UCIQE(水下彩色图像质量评价):偏高越好,主要反映色彩浓度与对比度;
  • PSNR 和 SSIM:有参考图时用,用来对比增强前后与真图的结构相似性,防过度增强。

我的实验表里,增强后的 UIQM 比原图普遍提升了约 30%,UCIQE 提升 15% 左右。SSIM 在有参考的仿真数据集上保持 0.75 以上,说明没有过度失真。

5.3 不同水质的适配性分析

需要特别指出的是,这套算法在蓝绿色近海水域的适应度很好;但到了某些内河浑浊黄色水域,暗通道先验本身会失效一部分,因为黄泥水的暗通道并不趋近于零。遇到这种情况,我建议先用一个简单的直方图匹配把颜色空间拉回接近海水的色调范围,再跑这套增强流程。实际项目里我用这个 trick 处理过一批浑浊水体视频,效果稳定了不少。

实际使用中的几个坑:多试几组图就会发现的规律

最后这部分,整理一下我在真实使用中反复踩过、也反复修正过的坑,希望帮你少走弯路。

6.1 灰度世界补偿和暗通道去雾的先后顺序不能乱

有人习惯先对原图做去雾再补偿颜色,我实测过,这样容易把暗通道先验估计的大气光 A 拉偏,透射率图出现奇怪的亮斑。因为去雾前的原图蓝绿通道过强,暗通道图会呈明显的蓝绿色偏向,此时按暗通道值排序选点,选出来的区域多半是蓝绿色强光区,A 就不准了。所以一定要先做波长补偿,再在补偿后的图上做去雾估计,这个顺序对最终效果影响非常大。

6.2 透射率下限设得太低会带来“死黑”区域

标准暗通道先验里透射率下限往往设成 0.1,空气场景无所谓,水下场景如果设 0.1,远处区域透射率非常小,恢复后的 J 就变成 (I - A)/t + A,t 太小时候颜色噪声被放大,暗部直接变成纯黑块。把这个下限设置到 0.4 附近,远区虽然还保留一点朦胧感,但至少不会出现大面积的“死黑”。鱼与熊掌不可兼得,宁可保色彩也不能让暗部崩掉。

6.3 图像上的小亮点和高光反射要提前处理

水下经常有白色悬浮物或者气泡,它们在高亮区域的暗通道值不高,但容易被“前 0.1% 亮像素”策略选进大气光估计的候选集。我的做法是在大气光估计之前,先对图像做一次中值滤波,把单个的高亮噪点抹掉,再走候选选点流程。这个操作对稳定 A 值非常有帮助,推荐任何水下场景都加上。

6.4 一个“低成本”处理视频流的思路

如果想把这套算法用到视频里,逐帧处理会非常慢,每帧的参数也可能抖动。我用的是“每隔 15 帧估计一次 A 和透射率统计参数,中间帧直接复用”的方式,实测速度提升了 5 倍左右,视觉上几乎察觉不到闪烁。在水下视频分析里,这种思路非常实用,推荐试试看。

后续可以扩展的方向

这套基础流程熟悉之后,能延伸的地方还不少。可以和直方图拉伸方法做融合,可以先对图像做白平衡再进暗通道,也可以考虑加入 Retinex 理论对红通道做额外补偿。我自己正在实验的方向是把这套流程里的透射率估计换成轻量网络版本,用 Matlab 调用深度模型做预处理,已经初有成效。如果有兴趣深入,建议以本文基础版本为起点逐步改造。

目前整个方案在实验室测试集上的表现我个人还是比较满意的,虽然计算量和轻量级算法没法比,但质量优先的场景下完全够用。如果大家实际跑起来也遇到参数调不准或者结果偏色的问题,欢迎按文中的排查思路先自我检查一下顺序和参数,很多时候问题出在细节上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询