水下图像增强实战:Matlab实现波长补偿与暗通道去雾

发布时间:2026/10/24 2:48:33
水下图像增强实战:Matlab实现波长补偿与暗通道去雾

手上拿到一张水下照片,第一反应往往是“这颜色不对”。不是偏蓝,就是偏绿,远处一片混沌,近处也像蒙了层纱。做水下图像增强,如果只把它当成普通图像去雾或者简单调色,效果基本都会翻车。我这一版方案走的是“波长补偿 + 去雾”的路线,用Matlab实现,目标是先恢复物理上被水吸收掉的颜色,再把水体散射造成的雾感压下去。这篇文章把整个思路、代码、参数和踩坑过程都写出来,给正在碰水下视觉、水下机器人、养殖监控、海洋生态观测这类项目的同学一个可以直接抄作业的参考。

1. 水下图像为什么又蓝又糊:先搞懂物理再谈算法

1.1 水对光的“作案过程”:吸收与散射

水下照片偏蓝偏绿,不是相机白平衡犯傻,而是水体对不同波长光的衰减能力不一样。普通白光的可见波段里,红光波长最长,能量低,在水中衰减极快;蓝光和绿光波长较短,穿透力强。所以相机在水下能接收到的主要是蓝绿通道的信息,红色通道的信息已经“半路夭折”。

这带来的问题很直接:图像颜色偏移是物理决定的,不是色温漂移那种后处理就能骗过眼睛的问题。你用一个常规白平衡算法去拉,拉出来的红色往往是假的、发灰的,尤其是暗部,会直接崩掉。

除了吸收,还有散射。水里悬浮的微小颗粒会把光线往各个方向弹,摄像机接收到的光里有一部分根本不是物体反射来的,而是这些颗粒散射造成的背景光。这就是大家常说的“海底雾感”,它让图像对比度急剧下降,远处场景的细节完全糊成一团。

所以水下图像增强要处理两个独立的东西:色彩失真和雾化降质。前者靠波长补偿,后者靠去雾。

1.2 为什么陆地去雾方法不能原样照搬

很多人在水下图像增强里第一反应是套用暗通道先验去雾,因为它在雾天图像上效果确实猛。但直接用在水下,几乎都会遇到两个问题:整体颜色更怪,以及恢复出来的红色通道变成灰紫色块。

原因要从暗通道先验的假设说起。这个先验认为,在没有雾的正常图像里,每个局部区域内,至少有一个颜色通道的像素值是趋近于0的,比如蓝天、阴影、彩色物体表面。但在水下图像里,红光已经先被水体吸收掉一大截,局部暗通道里最暗的往往是红色通道,但它不是“物体本身暗”,而是“被水衰减掉了”。此时暗通道先验会把这种衰减误判成场景变化,导致透射率估计偏差,最后恢复出的图像就会出现大面积的色斑和伪纹理。

暗通道去雾不是不能用,而是不能直接拿来就用。正确做法是先用波长补偿把颜色通道之间的能量关系大致拉回来,让红色通道的信息在一定程度上恢复出来,再去做透射率估计。这也是“波长补偿与去雾”组合的核心逻辑。

1.3 算法设计主线:先补偿,再去雾,最后校正

我实现的增强流程分三步走:第一步是对颜色通道做波长补偿,第二步是对补偿后的图像做去雾,第三步是用局部对比度增强做细节润色。

这里的关键是顺序不能乱。如果先去雾再做颜色补偿,去雾阶段的透射率估计会受偏色干扰,补偿时又会放大去雾引入的边缘伪影,结果就是两头不讨好。先补偿后去雾,去雾阶段面对的是一个颜色分布相对正常的输入,透射率估计的稳定性会高很多。

第三个步骤是可选的,但对大多数水下图像来说,去雾之后图像会比较“灰”,尤其是暗部细节不够。做一次局部对比度提升,视觉上会明显通透。实际项目中,这一步可以根据场景开关。

2. 核心算法拆解:波长补偿与暗通道去雾怎么落地

2.1 波长补偿:不要把每个通道都“拉满”

波长补偿的常见实现有基于物理模型的,也有基于统计的。物理模型会涉及水质参数、距离信息甚至深度传感器,精度高但工程上很麻烦。我这里用的是更稳的统计近似,核心思路是:假设图像中物体表面的平均反射率在三个颜色通道上是比较接近的,那么通道均值的差异可以近似看作水体衰减差异。

具体做法是计算红、绿、蓝三个通道的平均亮度,把绿通道或蓝通道作为基准,计算红色通道需要放大的比例。补偿强度的计算公式里引入一个参数 alpha,用来控制补偿量,避免直接把红通道拉满导致过曝。这个 alpha 我一般放在 0.3 到 0.8 之间,视原始图像偏色程度调整。

有一点要特别注意:在纯蓝或纯绿背景占大面积的图像里,通道均值会被大背景拉走,导致补偿强度算出来很极端。这种情况下不能只看全局统计,可以对图像做分块统计,或者手动设定红色通道的上限。保守一点,宁可补偿不足,也不要补偿过头。

2.2 暗通道先验去雾在水下的修正

暗通道去雾的标准公式是建立在雾天退化模型上的:观测图像等于场景辐射率乘以透射率,再加上天空光乘上 1 减透射率。要求解场景辐射率,需要先估计全局背景光,再估计透射率。

在水下场景里,我用补偿后的图像来做暗通道计算。暗通道取像素级三个通道的最小值,再做一个局部最小值滤波,这样得到的暗通道图能够反映雾的浓度分布。背景光不是简单取整张图最亮的像素,而是从暗通道最亮的前 1% 像素里取对应原始像素的最大值。这样能避免水下白色物体或人造光源干扰背景光估计。

透射率估计公式里有一个去雾强度参数的 omega,我默认取 0.85。omega 越大,去雾越猛,但水下场景如果太大,远景会出现黑块,所以一般不建议超过 0.9。透射率的下限我会限制在 0.2 到 0.3 之间,防止暗部区域在恢复时被噪声放大。

2.3 实现方案的选型建议

水下图像增强的方案多得很,有基于Retinex的,有基于深度学习生成对抗网络的,也有纯物理模型的。这一版选“波长补偿 + 暗通道先验 + CLAHE”做组合,主要考虑三点:

一是稳定。暗通道先验是经典方法,Matlab里不需要额外装第三方依赖,只要图像处理工具箱就能跑通。二是可控。每个环节都有明确参数,出问题时你能知道是哪一步搞坏了;换成深度学习方法,模型一跑,失败了都不知道该调哪里。三是通用。Matlab代码可以方便转为工程模块,后续要接实时视频处理,只需要把单帧处理函数循环调用即可。

如果你手上的水下图像色彩偏移特别严重,或者项目里有深度信息,那可以考虑把统计补偿替换成基于水深的物理补偿。但对大多数实验和预研阶段来说,我这个组合已经足够有效。

3. Matlab代码实现:一步一步搭出水下增强流程

3.1 环境准备与测试图建议

运行环境只需要MATLAB和图像处理工具箱(Image Processing Toolbox)。建议版本 R2019b 以上,主要是为了imguidedfilter和adapthisteq这些函数更稳定。不需要额外的深度学习工具箱。

测试图像最好不要拿一张网上的对比图就完事。水下图像质量参差不齐,有的偏蓝绿,有的带高亮度前景,有的光水相当浑浊。建议手边准备三类图:蓝色背景明显的、绿色背景明显的、以及有近景物体和远景分层的。这样调参时你能知道哪些问题是场景特有的,哪些是算法普适的。

注意:读取图像不要用imread后直接进算法。建议先把图像转为 double,范围归一化到 0 到 1,否则后面统计和除法都会有问题。

3.2 主函数代码与参数说明

我先直接给出一个可运行的主函数。代码里我刻意调整了可读性,方便你拆开看每一段的输入输出。

function out_img = underwater_enhance(input_img, opts) % UNDERWATER_ENHANCE 基于波长补偿与去雾的水下图像增强 % 输入: % input_img - uint8 或 double 的 RGB 图像 % opts - 可选参数结构体,字段见下文 % 输出: % out_img - double 类型的增强后 RGB 图像,范围 [0,1] if nargin < 2 opts = struct(); end % 参数默认值 if ~isfield(opts, 'alpha'), opts.alpha = 0.5; end % 波长补偿强度 if ~isfield(opts, 'omega'), opts.omega = 0.85; end % 去雾强度 if ~isfield(opts, 't_min'), opts.t_min = 0.25; end % 透射率下限 if ~isfield(opts, 'patch'), opts.patch = 15; end % 暗通道滤波窗口 if ~isfield(opts, 'use_clahe'), opts.use_clahe = true; end % 是否做CLAHE if ~isfield(opts, 'clahe_limit'), opts.clahe_limit = 0.03; end % CLAHE阈值 % 转换到 double 并归一化 I = im2double(input_img); I = min(max(I, 0), 1); % 1. 波长补偿 I_comp = wavelength_compensation(I, opts.alpha); % 2. 暗通道先验去雾 I_dehaze = dark_channel_dehaze(I_comp, opts.omega, opts.t_min, opts.patch); % 3. 对比度与颜色校正 if opts.use_clahe out_img = local_contrast_enhance(I_dehaze, opts.clahe_limit); else out_img = I_dehaze; end out_img = min(max(out_img, 0), 1); end

3.3 具体步骤的实现细节

波长补偿子函数:

function I_comp = wavelength_compensation(I, alpha) % 基于通道均值的波长补偿 r = I(:, :, 1); g = I(:, :, 2); b = I(:, :, 3); mr = mean(r(:)); mg = mean(g(:)); mb = mean(b(:)); ref = min(mg, mb); % 用较暗的蓝绿通道作为参考,避免亮背景干扰 % 理论上目标是把红通道均值拉到与蓝绿相当 scale = 1 + alpha * (ref - mr) / (mr + eps); r2 = r * scale; r2 = min(r2, 1); % 防止过曝 I_comp = cat(3, r2, g, b); end

这个补偿公式是“缩放红色”,不是“拉伸红色”。好处是绿色和蓝色完全不动,整体色调变化可控。如果希望绿通道也做一定的微调,可以延伸到所有通道补偿,但实测中只要红色拉回来,视觉效果就已经正常了。

暗通道去雾子函数:

function I_dehaze = dark_channel_dehaze(I, omega, t_min, patch) % 暗通道先验去雾(水下修正版) r = I(:, :, 1); g = I(:, :, 2); b = I(:, :, 3); % 像素级暗通道 dark_ch = min(min(r, g), b); % 局部最小值滤波 se = strel('square', patch); dark_ch = imerode(dark_ch, se); % 估计背景光:取暗通道最亮前1%像素中的最大强度值 A = dark_ch; thresh = max(A(:)) * 0.99; mask = A >= thresh; A_value = mean(I(repmat(mask, [1 1 3]) .* full? ), 'all');

这个写法有问题,我用更直接的写法替代。背景光最好这样取:

[rows, cols] = find(mask); A_vec = zeros(length(rows), 1); for k = 1:length(rows) A_vec(k) = max(I(rows(k), cols(k), :)); end A_global = max(A_vec);

但Matlab里这么做偏慢,下面代码中我采用一种更工程化的写法:直接从暗通道最亮的像素集合里取对应的最大像素值。

[~, idx] = sort(dark_ch(:), 'descend'); num_pix = max(1, round(0.001 * numel(dark_ch))); top_idx = idx(1:num_pix); A_global = zeros(3, 1); for c = 1:3 Ic = I(:, :, c); A_global(c) = max(Ic(top_idx)); end

估计透射率:

t = 1 - omega * dark_ch / max(max(A_global(:)), eps); t = max(t, t_min); % 恢复场景辐射率 I_dehaze = zeros(size(I)); for c = 1:3 Ic = I(:, :, c); A_c = A_global(c); I_dehaze(:, :, c) = (Ic - A_c) ./ max(t, 0.1) + A_c; end end

局部对比度增强子函数:

function I_out = local_contrast_enhance(I, clahe_limit) % 将图像转到LAB色彩空间,对L通道做CLAHE lab = rgb2lab(I); L = lab(:, :, 1) / 100; % L范围是0到100,转回0到1 L_enhanced = adapthisteq(L, 'NumTiles', [8 8], 'ClipLimit', clahe_limit); lab(:, :, 1) = L_enhanced * 100; I_out = lab2rgb(lab); end

这里有一个细节:rgb2lab和lab2rgb在Matlab里的数值范围容易记错,如果写成lab(:, :, 1)直接传给adapthisteq,会被当作0到255范围,输出直接偏色。所以先除以100归一化,处理完再乘以100还原。这个细节我调了很久才意识到,网上很多代码都没提。

3.4 输出分析与对比方法

增强完成后,不能只看“看起来顺眼”。工程上要量化效果,建议对比三个指标:平均梯度、信息熵、色偏程度。平均梯度反映边缘清晰度,信息熵反映细节丰富度,色偏程度我用LAB色彩空间的a和b分量均值距离零点的远近来表示。

% 增强前 img_in = im2double(imread('test.png')); img_out = underwater_enhance(img_in); % 平均梯度 gradient_in = mean(gradient(double(rgb2gray(img_in)))); gradient_out = mean(gradient(double(rgb2gray(img_out)))); % 信息熵 entropy_in = entropy(img_in); entropy_out = entropy(img_out); % LAB色偏 lab_in = rgb2lab(img_in); lab_out = rgb2lab(img_out); ab_in = mean(std(lab_in(:,:,2)) + std(lab_in(:,:,3))); ab_out = mean(std(lab_out(:,:,2)) + std(lab_out(:,:,3)));

这只是简版评价,严谨的话可以再算水下图像质量指标,但Matlab官方没有直接函数,需要自己实现或找第三方代码。预研阶段用这三个指标足够排查大问题了。

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

4.1 图像还是偏绿或偏蓝怎么办

先确认是不是红色补偿强度不够。把opts.alpha从 0.5 提升到 0.7 或 0.8 再跑一次。如果补偿后画面出现大面积红色伪影,说明补偿过头了,降回 0.3。很多时候偏色没解决不是参数问题,而是背景光估计被大面积深色区域带偏。这种情况下可以手动为背景光设定一个上限,比如不让它超过图像最大亮度的90%。

另外要注意,如果原图本身在采集时的白平衡已经做过一次修正,那补偿强度应该比正常值更低,否则会二次偏色。

4.2 增强后噪点爆炸怎么办

水下图像暗部噪声本来就大,去雾里透射率越接近0,恢复时噪声放大越严重。我一般会在透射率初步算完之后加一个低通滤波,用medfilt2或imguidedfilter对透射率图做平滑。透射率图平滑了,恢复出的边缘就不会出现颗粒状噪声。

还有一个非常实用的做法:最后一步 CLAHE 的ClipLimit不要设置太高。我在0.03到0.05之间取。设置过大会导致局部对比度过强,暗部噪点被一起放大,看起来像“油画感”。

4.3 参数对结果的影响与选择策略

我把实际调参经验整理成一张速查表,方便遇到问题时快速定位。

现象优先调整参数调整方向
整体偏蓝偏绿alpha增大
红色区域过曝alpha减小,或将 r2 上限设为0.95
远景还是有雾omega增大
天空/水体背景变黑omega减小
暗部噪点明显t_min增大
图像模糊不清use_clahe打开,并适当增加clahe_limit
局部色斑严重patch适当增大到20左右

特殊场景需要特殊处理。比如拍摄目标本身是红色物体时,如果全局红色通道本身不低,那补偿算法会过冲,导致整张图像红色饱和。此时可以分区域补偿,对红色物体所在区域降低补偿系数。这项工程里我常用前景分割或亮度加权来做,代码量会大一点,但效果扎实。

4.4 处理耗时与控制方法

单张图像处理速度主要卡在暗通道的局部最小值滤波上。如果测试图像是2000万像素,strel('square', 15)的imerode跑起来会比较慢。优化的办法有三个:

先把图像缩到不超过 800 像素宽的小尺寸,跑完全流程后,再用imresize把输出恢复到原始尺寸。实测这张方式在视觉效果上损失很小,速度提升却非常明显。其次,把 patch 从 15 降到 11,速度会加快,但透射率图会更粗糙。最后,如果项目允许,可以在暗通道滤波后用imresize把透射率图缩小,再imresize放大,减少滤波开销。

如果要处理视频,不要每一帧都重新估计背景光。可以在每几帧里抽一帧计算背景光和透射率,其余帧复用相同参数做恢复。这样能保持画面稳定,不会一帧亮一帧暗。

我个人在实际项目里的体会是,水下图像增强的调参并不是一次到位的。你先用固定参数跑一批图,把问题图分好类,再针对每类场景微调参数,比对着单张图调出来的效果要稳定得多。这个流程看起来多跑几次代码,但对于实际落地很有帮助。

最后再分享一个技巧:增强前后对比图不要只拼两张原图,可以把差值图也输出出来,看看算法在哪个区域改了最多、哪个区域根本没动。很多调参问题,看一眼差值图就明白是去雾过头还是补偿不足。这个步骤能帮你省掉一半试错时间。