水下图像增强这个方向,我是从某图像处理模拟项目开始接触的。当时拿到一批水下拍摄的视频帧,画面蓝得发闷,红色通道像被硬生生“挖掉”了一块,细节信息几乎全埋在雾一样的散射里。我起初以为这跟陆地去雾是一回事,直接把暗通道先验套上去跑,结果增强后的图像不是偏红到失真,就是噪声被无限放大,折腾了好几天才意识到:水下图像的退化机制跟雾天完全不是同一个逻辑。后来把波长补偿和去雾两条思路认真梳理了一遍,重新设计算法链路,才真正把这套方案跑通。这篇文章就把整个研究过程和Matlab代码实现完完整整复盘一遍,内容包括水下图像的退化机理、波长补偿与去雾的融合思路、关键代码框架、实验评估方法,以及我在实际调试中踩过的各种坑。正在做水下图像增强研究、或者只是想把水下视频画面处理得更干净的读者,都可以拿这篇做参考。
1. 水下图像的退化真相:不只是“暗”,是“丢颜色”
我一开始犯的错误,就是把水下图像直接当成“带雾的普通照片”来处理。实际上,水下图像质量下降的机制比陆地雾天复杂得多,它由吸收和散射两个物理过程共同造成。理解这两个过程,你才会明白为什么要做波长补偿、为什么单纯搬去雾算法一定会失效。
1.1 吸收:不同波长的光在水中“寿命”不同
水体对光的吸收具有明显的波长选择性。光在水中的衰减系数随波长变化很大:红光波长大约在620纳米附近,水对它的吸收最强,大概在水下5米左右的深度,红色能量就已经衰减大半;绿光波长更短,衰减相对慢一些;蓝光在水中穿透能力最强,能到达更深的水层。这也是为什么几乎所有水下照片都呈现蓝绿色——并不是相机白平衡没调好,而是红色信息在物理层面就已经严重丢失了。
这个规律对算法设计的影响是决定性的。你可以把水下成像理解为:一张照片的RGB三个通道里,R通道被水“抽走”了大部分能量,G和B通道损失相对少一些。如果用一个统一的去雾模型对三个通道做完全相同的处理,等于默认三个通道的退化程度是一样的,这个前提在水下完全不成立。波长补偿方法的核心逻辑,就是先把这种“各通道衰减不均”造成的偏色问题补回来,让R通道的信息尽量恢复到接近G、B通道的状态,之后再去处理散射带来的模糊。
值得注意的是,红光衰减并非在所有水下场景中都一样严重。清水和浑浊水、浅水区和深水区,各通道的衰减比例差异很大。因此,波长补偿需要设计成自适应估计的形式,而不是用一个固定的增益把红色通道强行拉高。后面我会详细展开具体的自适应策略。
1.2 散射:对比度下降的元凶
除了吸收,水体中悬浮颗粒对光的散射同样严重。你可以把散射想象成汽车远光灯在大雾里的效果:光线在传播路径上被微粒反复弹射,一部分原本应该到达相机的光被弹开,另一部分不属于该像素的环境光被弹进传感器,最终导致图像对比度急剧下降、边缘细节模糊、整体笼罩在一层灰蒙蒙的面纱中。这种现象和水雾天气非常像,也是“水下去雾”这个说法成立的物理依据。
水下散射还有一个特点:它不只是让图像变灰,还会加剧色彩偏移。因为短波长蓝光的散射相对较强,水下图像的背景区域往往呈现浓郁的蓝色调。这就形成了一个双重困境——颜色和对比度的问题交织在一起,单独处理哪一个都很难得到满意结果。
1.3 陆地去雾算法为什么“水土不服”
经典暗通道先验在陆地雾天图像上效果很好,它基于一个统计假设:在无雾图像的局部区域里,至少有一个颜色通道的亮度值非常低、趋近于零。这个假设在陆地场景中大部分时候成立,比如阴影、深色物体、彩色物体的某个暗色通道等,都能提供足够的暗像素信息。
但水下图像的统计特征完全变了。由于水体对红光的选择性吸收,深水区域的图像几乎每一个像素的R通道都很小,不再是“局部稀疏存在”的暗像素,而是全图范围的结构性缺失。当计算三通道最小值得到的暗通道时,数值普遍偏高,暗通道先验的假设被破坏,透射率会被严重高估。强行套用经典算法的结果,往往是颜色进一步偏移、对比度矫正过度,甚至出现可笑的偏红伪影。
因此,水下增强方法普遍会对暗通道先验做改进,其中最常见的方法是转向“红通道先验”,把原本的RGB三通道最小值改为对R通道做补偿后再提取暗信息。这个改动的本质,是认清水下环境中R通道才是信息最稀缺、最容易被散射影响覆盖的通道,必须优先处理它。
2. 波长补偿与去雾:两条技术路线怎么捏在一起
既然水下的问题既有“颜色通道衰减不均”,又有“散射导致的对比度下降”,最自然的思路就是把两个步骤串联起来:先做波长补偿解决偏色问题,再做去雾处理解决模糊问题。听起来简单,但真正实现时会遇到不少设计抉择,下面把关键问题拆开讲。
2.1 波长补偿的自适应设计思路
最朴素的波长补偿办法是全局统计法:计算G、B通道平均亮度与R通道平均亮度的比值,然后放大R通道乘以一个系数。这种做法在场景简单、光照均匀的理想图像上有点效果,但一到真实水下照片就会翻车。原因在于,水下场景中经常存在大片蓝色背景区域,这些区域的R通道本底信息几乎为零,如果按照全局平均比值去补偿,背景区域会被强行灌入大量红色增益,最终导致蓝色背景变成诡异的紫红色,噪声也跟着被放大。
更稳妥的做法是用“亮区域环境光估计”代替全局均值。我们可以先估计出场景中的环境光(可以近似理解成水体背景光),然后把它与参考的白色或者灰色目标对比,得到各通道的补偿比例。相当于问一个很直接的问题:如果这张图里有一片理论上应该是白色的物体,它在水中被吸收后变成了什么颜色?补偿的目标就是把这个颜色拉回白色,这样整幅图的色彩就自然了。
在实际项目中,我选用了基于灰度世界假设修正的变体。灰度世界假设认为,一张色彩分布足够丰富的自然图像,其三个颜色通道的平均值应该趋于一致。水下图像因为红色缺失,三通道均值严重不平衡,把这个偏差通过比例系数修正,就得到一个简单但不失稳健的补偿结果。关键点是设置一个补偿上限,防止R通道被无限拉高,同时只在色彩信息丰富的区域做强补偿,在深水背景区域适当降低补偿强度,避免噪声泛滥。
2.2 改进后的暗通道先验如何构建
完成了波长补偿,接下来进入去雾环节。前面说过,经典暗通道先验在水下图像上失效,这里要换成改进版本。核心做法是:在提取暗通道时,不再对RGB三通道取最小值,而是单独使用补偿后的R通道来估计暗通道信息。
为什么只盯R通道?因为水下图像中最先丢失信息的是R通道,它的暗像素统计最接近陆地图像中暗通道先验的适用条件。对于非红色物体,R通道的值本来就低,水下环境又额外衰减了它,这种结构性暗信息正好符合暗通道先验假设。对于白色或者其他浅色物体,R通道值相对较高,可以作为透射率估计的参考。这种方式比简单取三通道最小值要稳得多,也是“红通道暗通道先验”的核心思想。
透射率估计公式仍然沿用大气散射模型的框架:
[ t(x) = 1 - \omega \cdot \min_c \left( \frac{I_c(x)}{A_c} \right) ]
其中 (\omega) 是保留远景雾感的常数因子,一般取0.85到0.95。在红通道暗通道先验的框架下,不再对三通道都套用这个公式,而是主要依赖R通道的信息估计,再把这个透射率用于整幅图像的三个通道。这样既解决了水下场景统计假设失效的问题,又保持了模型简洁。
2.3 算法的完整处理流程
把这套逻辑整合到一起,整个算法流程可以分成五个阶段:
- 输入原始水下图像,转换成double类型并归一化到[0,1]区间。
- 估计环境光,并据此计算R通道的波长补偿系数,对RGB三通道分别做补偿,得到初步校正图像。
- 在补偿后的图像基础上提取红通道暗通道,估计环境光值,并计算初始透射率图。
- 用引导滤波对透射率图做细化,消除块状伪影,保持边缘结构。
- 根据大气散射模型重建图像,再做一次对比度拉伸和色彩修正,输出最终增强结果。
这个流程的每一步都有明确的物理意义,不是盲目叠加操作。波长补偿解决“颜色丢没丢”的问题,暗通道去雾解决“图像糊没糊”的问题,最后的色彩修正解决“输出能不能看”的问题。
3. Matlab代码实现拆解:关键步骤一读就懂
算法跑通之后,代码的可复现性就是最重要的事了。Matlab自带的图像处理工具箱提供了最常用的滤波器、形态学操作和引导滤波函数,非常适合快速验证算法。我用的版本是R2022a,后面所有代码在这个版本及相近版本上都能直接运行,不需要额外安装第三方工具包。
3.1 读图与数据预处理
% 读取原始水下图像 I_original = imread('underwater_sample.png'); % 转换为double并归一化到[0,1] I = im2double(I_original); % 分离三个颜色通道 R = I(:, :, 1); G = I(:, :, 2); B = I(:, :, 3);这一步看起来简单,但有个细节值得多说一句。im2double把8位整数图像归一化到[0,1],这是后续所有浮点运算的前提。如果直接用uint8做运算,中间结果会频繁被截断,透射率估计和补偿计算都会引入不必要的误差,最终输出的图像容易出现色带和奇怪的边缘。
读图之后,建议先观察一下三个通道各自的直方图。我通常会在调试阶段用imhist分别查看三通道分布,快速判断红色通道是否大面积接近零,绿色和蓝色通道的分布重心在哪里。这一步能帮你确认当前图像属于“深度较大的蓝绿主导场景”还是“浅水区红绿蓝相对均衡的场景”,从而决定补偿强度是否需要加大。
3.2 暗通道计算与环境光估计
提取红通道暗通道最直接的方法是做最小值滤波。这里使用ordfilt2或imdilate都可以,但要理解其中的区别。
function dark = dark_channel_red(R, patch_size) % 对红通道做最小值滤波,获得暗通道 dark = ordfilt2(R, 1, ones(patch_size, patch_size), 'symmetric'); endordfilt2的第二个参数设为1,表示取局部最小值,等价于最小值滤波。选择对称填充方式可以避免图像边缘区域出现明显的黑色边框。窗口大小直接影响暗通道的粒度:窗口太小,暗通道估计容易受噪声干扰;窗口太大,透射率图会丢失细节。实际项目中,对于常见尺寸的水下图像,我通常把patch_size设为15到25之间,具体值可以按图像尺寸的1%到2%估算。
环境光的估计逻辑有一点绕,但很关键。暗通道中亮度最高的像素,通常对应的是图像中最“不透光”的雾感区域,这些区域能够代表环境光。做法是取暗通道图中亮度最大的前0.1%像素位置,然后在原始图像中找这些位置的最大像素值作为环境光。
function A = estimate_atmospheric_light(I, dark) % 将暗通道展平并确定阈值 [height, width] = size(dark); num_pixels = height * width; top_num = max(1, floor(num_pixels * 0.001)); dark_flatten = dark(:); [~, idx] = maxk(dark_flatten, top_num); % 在原始图像中取这些位置的各通道最大值 A = zeros(1, 3); for c = 1:3 channel = I(:, :, c); A(c) = max(channel(idx)); end end经验之谈:环境光估计是整套算法里最容易“阴沟翻船”的步骤。水下图像里如果有白色的鱼、砂底、潜水员装备等高亮目标,这些位置容易被误判为环境光,导致透射率估计整体偏低。我试过在测试集里加入一张带白色鱼身的图片,算法输出的增强结果明显发灰,后来在环境光估计前加了一步简单的去高亮保护:在暗通道图中排除掉原图多个通道同时接近峰值1的像素,问题才得到缓解。这个细节在论文里通常一句话带过,实际工程中却决定最终效果。
3.3 透射率估计与引导滤波细化
有了环境光,就可以计算透射率图。基础公式的Matlab实现非常简洁:
omega = 0.9; % 对红通道计算归一化暗通道 normalized_R = I_compensated(:, :, 1) ./ A(1); dark_norm = dark_channel_red(normalized_R, patch_size); t_initial = 1 - omega * dark_norm;直接用这个透射率重建图像,必然会出现块状伪影。因为最小值滤波是基于局部窗口的,窗口边缘处的透射率会突变,导致恢复图像在边缘附近颜色跳变。解决方法是引入引导滤波,把原来的透射率图结构细化。
% 用亮度图作为引导图,细化透射率 I_gray = rgb2gray(I_compensated); t_refined = imguidedfilter(t_initial, I_gray, ... 'NeighborhoodSize', 30, ... 'DegreeOfSmoothing', 0.001);引导滤波的作用通俗点说,就是既让透射率图变得平滑,同时保留原图的边缘轮廓。这也是它比普通高斯滤波更适合做透射率细化的原因。这里的NeighborhoodSize和DegreeOfSmoothing是两个需要测试的参数。窗口太大,透射率变化过于平缓,远距离区域的雾感会被过度去除;平滑系数太大,边缘信息丢失,图像会出现光晕。
我自己调试时总结的经验是:NeighborhoodSize取图像短边长度的1/20左右,DegreeOfSmoothing取0.001到0.01之间,先固定一个参数,只调另一个,记录三张不同场景图像的输出,观察细节和颜色表现,再微调。不建议一上来就在整批测试集上调参,容易陷入“过拟合测试集”的陷阱。
3.4 图像重建与色彩恢复
透射率细化完成之后,逐通道重建无散射图像:
t0 = 0.1; J = zeros(size(I_compensated)); for c = 1:3 J(:, :, c) = (I_compensated(:, :, c) - A(c)) ./ max(t_refined, t0) + A(c); endmax(t_refined, t0)是防止透射率接近零时除零运算导致噪声爆炸。t0太小容易引入大量噪点,太大则图像仍然偏灰。0.1是一个常用的经验值,但如果你发现增强结果在天空或者背景区域出现斑点,可以谨慎提高到0.2左右,会牺牲一点对比度换取更干净的背景。
去雾重建之后,图像颜色往往还需要一次全局调整。我会做两步后处理:第一步是限制对比度自适应直方图均衡(CLAHE),增加局部对比度;第二步是饱和度与亮度的适度校正。
% CLAHE 增强亮度通道 J_lab = rgb2lab(J); J_lab(:,:,1) = adapthisteq(J_lab(:,:,1), 'NumTiles', [8 8], 'ClipLimit', 0.01); J_final = lab2rgb(J_lab);这里选择在Lab空间的亮度通道上做直方图均衡,而不是直接在RGB空间处理,是为了避免颜色被过度拉伸导致偏色。我见过一些实现直接在RGB三通道上分别做直方图均衡,结果偏色问题反而更严重,因为红通道信息本来就不足,强行均衡会制造出大量不自然的红色噪声。
3.5 核心参数速查表
| 参数 | 推荐取值范围 | 影响 | 备注 |
|---|---|---|---|
| patch_size | 15 ~ 25 | 暗通道粒度,影响透射率细节 | 按图像宽高的1%到2%估算 |
| omega | 0.85 ~ 0.95 | 保留远景雾感的程度 | 调大则去雾更强,远景更易偏暗 |
| NeighborhoodSize | 20 ~ 40 | 引导滤波平滑范围 | 按图像短边1/20粗调 |
| DegreeOfSmoothing | 0.001 ~ 0.01 | 边缘保持与平滑的平衡 | 过大则边缘模糊 |
| t0 | 0.1 ~ 0.2 | 透射率下限,抑制噪声 | 背景噪声明显时可适当调高 |
这张表是我花了将近两周时间调出来的参数域,所有的值都在十张不同风格的水下测试图像上验证过。当然,不同数据集的最佳参数肯定有差异,但基本都在这个范围内,可以作为起步配置。
4. 实验评估:主观质量与客观指标的双重检验
算法写完,不能光靠肉眼看两三张效果图就下结论。水下图像增强领域有一个常见的毛病,就是只挑效果最好的图展示。要评价一个算法到底行不行,得从测试集搭建、客观指标选择和结果分析三个层面系统去做。
4.1 测试集怎么搭才不容易“骗自己”
我的做法是收集了80张水下图像,涵盖三类典型场景:蓝色清澈水域的远景、绿色浑浊水域的近景、以及存在高亮物体(比如浅色砂底或白色鱼体)的干扰场景。每一类大约20多张,外加一部分带有人工合成参考图像的数据用于计算PSNR和SSIM。
这里要提醒一句:真实水下图像没有“原图对”,因为水下增强问题本身是病态逆问题,你无法知道场景在理想光照下的真实颜色。因此,主观视觉评价和UCIQE、UIQM这类无参考指标才是重点。如果想计算PSNR和SSIM,只能退而求其次,使用人工合成的退化图像——把陆地清晰图像按水下衰减模型加散射、加色偏,生成模拟水下图像,然后让算法去恢复。这种做法适合验证算法的物理合理性,但不能完全代表真实水下的表现。
4.2 无参考指标怎么选、怎么统计
水下图像领域最常用的两个无参考指标是UCIQE和UIQM。UCIQE主要衡量色偏、对比度和饱和度三个维度的综合质量,UIQM则融合了清晰度、色彩度和对比度。在实际评测中,我以UCIQE为主,UIQM为辅,因为前者的计算简洁且对颜色恢复的反映更直接。
| 方法 | UCIQE(均值) | UIQM(均值) | 运行时间 |
|---|---|---|---|
| 原始图像 | 0.421 | 2.35 | - |
| 仅暗通道先验 | 0.456 | 2.61 | 0.8秒 |
| 仅波长补偿 | 0.472 | 2.74 | 0.3秒 |
| 波长补偿 + 去雾(本文方法) | 0.513 | 3.12 | 1.4秒 |
上表是某模拟项目测试集中一批典型图像的参考统计值。可以看出,波长补偿和去雾单独使用时指标提升有限,二者结合后两个指标都有明显上升。但我也要强调,这类指标不能完全代表人的视觉感受。有些图像经过过度增强后指标数值很高,视觉上却已经出现明显的偏色和伪影,反而不如指标稍低但自然的结果。
4.3 结果分析中的“常见说谎方式”
做对比实验时要特别小心几个陷阱。第一,不要用一个场景的几张图就得出“本方法优于某某方法”的结论,至少要在全测试集上统计平均指标,并注意方差。第二,对比方法要在同样条件下运行,包括同样的输入格式和参数设置,否则对比不公平。第三,展示结果时不要只挑最漂亮的图,把典型的失败案例也放出来,反而能说明你对算法边界有清晰认识。
我在实际评测中就遇到过这样的问题:某个对比方法在绝大多数图像上表现平庸,但在少数几张高对比度图像上指标非常高,如果只看这几张效果图,很容易产生误判。后来我把所有图像的指标做成箱线图,才看清楚算法稳定性的真实情况。
5. 工程落地中的坑与改进方向
算法在测试集上表现不错之后,我原以为可以顺利交付到实际项目中,结果应用到真实水下视频时又暴露出一堆问题。这些坑在纯论文复现环节几乎不会被提到,但在工程化阶段却决定了一个算法能不能真正投入使用。
5.1 最容易翻车的三个环节
第一个坑是大气光被高亮物体带偏。前面说过,白色砂底、白色鱼体和其他高亮区域会干扰环境光估计。应对策略是对暗通道图中的候选位置做一次RGB通道一致性检查,剔除那些三个通道都很亮、明显不像水体散射光的像素。
第二个坑是红色通道增益过度导致噪声放大。波长补偿系数一旦拉得太高,暗部区域的红色噪声会被成倍放大,图像出现明显的杂色斑点。我的解决办法是在补偿公式里加入一个基于通道局部方差的保护项:局部区域红色通道方差太小,说明那里几乎没有有效红色信息,补偿强度就应该降低,而不是强行去拉高。
第三个坑是视频处理时的亮度闪烁。对视频的每一帧独立做增强,帧与帧之间的环境光估计结果会发生抖动,导致整体亮度不连贯。我在处理视频时会对环境光估计和透射率做时域平滑,比如对连续几帧的A值取滑动平均,这能显著降低闪烁感,代价是算法对突然的场景变化响应变慢,需要你在实时性和稳定性之间做权衡。
5.2 不同水质与深度的自适应策略
水下图像增强最大的挑战之一,就是没有一套参数能在所有水况下通吃。清澈蓝水、浑浊绿水和港口污水对应的衰减系数差异极大。目前我采用的方案是在输入阶段增加一个水质粗略分类:根据G/B通道比值和整体亮度统计,估算当前场景的水体类型,然后动态切换补偿强度和去雾参数。这是工程上折中的做法,计算量小,稳定性高。更精细的做法是在模型中加入深度估计,但这个方向复杂度较高,目前还在测试中。
5.3 与深度学习结合的一点思考
传统优化方法的最大优势是可解释性高、不需要数据训练、在机器上就能实时跑。它的上限也受限,面对极端浑浊场景,纯物理模型的表达能力不足。最近我也在研究轻量级网络和传统方法的结合,思路是让网络预测透射率残差,而不是直接端到端输出增强图像,这样既保留了物理约束,又吸收了数据驱动的非线性表达能力。从目前的实验看,这个方法在极端场景下的稳定性比纯物理模型好,但训练数据的收集本身又是一个大工程。
最后再分享一个调试小技巧
如果你想快速验证自己的Matlab实现是否靠谱,我建议准备一张视觉上“不那么严重”的浅水图像,和一张深蓝背景占大面积的典型深水图像,拿这两张图专门做异常测试。浅水图能暴露出补偿过度和偏色问题,深水图能暴露出噪声放大和大气光误估计问题。这两个极端都通过了,算法在大部分常规场景里基本不会翻车。另外,每次修改参数后,把处理前后的图像和指标结果统一存到一个对比目录里,按日期命名,一个月后回头翻这些记录,你会发现很多调试经验都是在这些对比记录里总结出来的。水下图像增强看起来坑多,但只要你把物理退化机制想透,再耐心把各个环节的细节打磨好,这条路是可以走通的。