水下图像增强这个方向,很多人一开始都会陷入一个误区:以为套一个白平衡或者直方图均衡化就能搞定。但真正拿着潜水相机或者ROV采集回来的视频帧跑一遍,就会发现结果要么偏绿偏蓝压不住,要么对比度是上来了但噪点也跟着爆炸,细节区域出现一圈一圈的光晕。我大概在半年前接了一个模拟项目X,核心任务是做水下图像与视频增强,当时选定的技术路线就是基于融合的多分支增强算法,配套Matlab代码做原型验证。这篇博文就完整复盘一下这个方案从物理原理、算法设计到工程落地的全过程,包括我在实际调试里踩过的坑和最终采用的参数策略,希望对正在做水下视觉或者低照度图像增强的朋友有参考价值。
1. 水下图像为什么那么“难受”:吸收、散射的双重打击
水下图像退化的物理根源,不是传感器不行,而是光在水里的传播路径本身出了问题。这个和普通陆地上的雾天成像有相似之处,但水下要更复杂,因为除了散射,还要叠加波长相关的吸收效应。
1.1 吸收效应:红光最先消失,图像整体偏蓝绿
水下环境里,不同波长的光衰减速度差别很大。红光波长长,能量低,大约在水下几米深度就开始明显衰减;绿光稍好一点;蓝光在水里穿透能力最强。这个物理规律直接导致了水下图像的典型色偏——浅水区偏绿,深水区偏蓝,十几米往下的场景基本就是一片蓝绿色调,红色通道的信息几乎全部丢失。
这个特性影响的不只是“好看不好看”的问题,它对后续所有视觉处理都是灾难。因为红色通道信噪比极低,你在这个通道上做任何增强都会把噪声放大;但如果不处理红色通道,颜色还原就无从谈起。所以单靠一个全局白平衡很难同时兼顾颜色恢复和噪声抑制,这也是我后来转向融合方案的根本原因。
1.2 散射效应:前向散射模糊画面,后向散射制造雾霾
散射分为前向散射和后向散射两类。前向散射是光在传播过程中被微粒偏折后仍然到达传感器,但方向乱了,带来的后果是图像边缘模糊、细节发虚;后向散射则是光源发出的光直接被水中微粒反射回相机,相当于在成像路径上挂了一层“亮纱”,整体对比度被严重压低,远处的物体像蒙了雾。
这两种散射叠加,效果就是水下图像同时存在“模糊”和“低对比度”两个独立的问题。模糊需要去卷积或者高频增强,低对比度需要直方图拉伸或者去雾处理,它们的处理手段是不同方向的。这也是为什么单一算法很难同时解决水下图像的全部退化问题——你需要在多个维度上分别处理,然后再把结果合理地合起来。
1.3 从成像模型看增强的本质
水下成像模型通常写成这样的简化形式:
J(x) = I(x) * t(x) + A * (1 - t(x))J是传感器接收到的退化图,I是理想清晰图,t是透射率图,A是背景光。你可以把它理解为:最终看到的像素 = 目标物体的原始光衰减到传感器 + 路径上散射光叠加进来。水下增强的本质,就是从退化的J里反推I,但t和A都是空间变化的,全局估计一个常数根本不准。
融合式增强的聪明之处在于,它不强行去求解这个病态反问题,而是通过多个针对不同退化子问题的增强分支,各自生成一个“局部正确”的结果,再通过权重融合拼出一个整体满意的输出。这个思路我在第三节会展开讲,先聊聊纯单一算法做水下增强到底有哪些硬伤。
2. 单算法方案的三个死穴,以及它们给融合留下的空间
我在动手写融合算法之前,把市面上常见的水下单算法方案都跑了遍实验。结果非常规整地暴露了三个问题,这三个问题就是融合路线最有力的论据。
2.1 死穴一:全局白平衡在深水区直接失效
灰度世界假设在陆地场景很好用,因为场景颜色统计上大致中和。但水下不是这样,深水区图像里蓝绿通道的能量远高于红色通道,灰度世界假设会把这个极端偏置当成真实色偏去校正,结果就是把整张图往红色方向猛拉,产生大面积不自然的红色或紫色伪影。
我试过用经典的白平衡算法跑一张水下6米深度的帧,输出图的背景区域出现了明显的品红色偏,肉眼一看就知道是“后期拉过头了”。后来我把白平衡只当作融合的一个分支,而不是最终输出,就解决了这个问题——分支允许“过度校正”,因为融合权重重绘时会把不合理的区域压下去。
2.2 死穴二:去雾算法把颜色洗成灰色
水下图像的“雾感”和陆地雾天成像确实有相似性,所以很多研究直接用暗通道先验去雾。暗通道先验在水下也有一定效果,但它基于的是“局部至少一个通道的强度很低”这个陆地场景统计规律,水下图像的蓝色通道往往整体偏亮,导致透射率估计不准。
跑出来的典型结果是什么呢?整体对比度确实提升了,但颜色变成了一种奇怪的灰蓝色或者灰绿色,色彩饱和度大幅下降。说白了,去雾算法擅长体量“亮纱”,但不认颜色,它会把颜色信息当作需要去掉的“雾”的一部分。融合方案则可以让去雾分支只负责结构恢复,让颜色恢复交给另一个分支来做,各管各的事。
2.3 死穴三:直方图均衡化放大噪声和色偏
直方图均衡化属于像素级对比度调整,它的优点是实现简单、实时性好,但缺点同样突出:全局均衡会让占主导的蓝绿色通道被过度拉伸,导致色偏加重;如果做的是局部均衡(CLAHE),虽然细节有所提升,但也会把水下原本就很明显的噪声一起增强,产生很强的颗粒感。
我在测试里发现,单跑CLAHE的水下视频帧,画面噪点比原图还明显,尤其是在深水区的暗部区域。这说明水下增强不能只盯着对比度,必须在增强信号的同时控制噪声。融合算法在权重设计时可以把“噪声水平”作为一个隐式惩罚项,这比在单一算法里手工调CLAHE的对比度限制参数要灵活得多。
2.4 三条路线的合流:融合可能是工程上最稳的答案
单算法各有侧重,但没有任何一个能在所有水下场景里稳定输出。实际问题里你面对的素材可能是浅水浑浊、深海蓝调、人工光源近距离拍摄等等,没有一套参数的单一算法能横跨这么多场景。融合的工程优势在于:不是让一个算法去讨好所有情况,而是让各自擅长一个方面的算法“分工”,再让权重机制动态决定谁在哪个区域说了算。这就是我后来选融合路线的真正理由。
3. 融合增强的核心逻辑:两道“补丁”如何互补而不是互相干扰
融合方案的框架不复杂,说起来就三步:生成多个增强候选、计算权重图、多尺度融合。但落地的时候,每一步都有值得展开的细节。
3.1 分支设计:白平衡分支负责颜色,对比度分支负责细节
我的方案采用两个核心分支:
第一个分支是颜色恢复分支,做法是对RGB三个通道分别做灰度世界校正,再配合一个饱和度拉伸。这个分支的目标就是让图像颜色回归自然,哪怕产生过度校正也可以接受,后续权重会收拾残局。
第二个分支是细节与对比度增强分支,做法是先用CLAHE对亮度通道做局部对比度增强,再叠加一个高频增强滤波(类似于unsharp masking的变体)。这个分支的目标是让纹理、边缘、远处的结构重新清晰起来,对颜色不作任何承诺。
两个分支输出的图像,一张颜色自然但可能偏平淡,一张细节突出但颜色奇怪,单独看都不完美,但它们的优势区域恰好是互补的。融合的作用,就是用权重图把每张图里“表现好的地方”挑出来,拼成一张综合最优的结果。
3.2 为什么不能用简单的平均值合并
可能有人会想:直接把两个分支的结果按50%透明度叠加不就行了?我在早期实验里确实这么试过,效果只能说勉强能用,但问题很明显:颜色自然区域的红色校正会被细节分支拉回来一部分,细节突出的纹理区也会被颜色分支“冲淡”,两边互相拖后腿。简单平均等于把两个分支的优点都稀释了一半。
正确做法是像素级的动态加权:在某个像素位置上,如果颜色分支的饱和度更高、对比度更合适、曝光更合理,就多采信颜色分支的结果;反之则采信细节分支。这个“采信程度”的分布,就是权重图。权重图本身也是一张灰度图,每个像素值代表对应候选图像在该位置的贡献比例。
3.3 权重设计的三个考量维度
我在生成权重图时,综合了三个维度的信息:
对比度权重,反映局部纹理强度,细节分支在纹理丰富区域通常能拿到高权重;饱和度权重,反映颜色鲜艳程度,颜色分支在恢复色偏后的区域会拿到高权重;曝光度权重,评价像素值是否落在合理曝光区间,避免过曝和欠曝区域被错误放大。
三个权重先逐像素相乘,再归一化到和为1。这一步的matlab实现很直接,但要注意归一化的数值稳定性问题,如果三张权重图在某些区域同时接近0,归一化会出现病态结果。我的解决办法是在分母上加一个很小的常数项(大约1e-6),同时把结果强制截断到[0, 1]区间。
4. 我在Matlab里怎么搭融合管线:从图像读取到分量处理
这一节直接上代码和流程。完整的Matlab实现不算复杂,但每一步的细节都值得注意,尤其是类型转换和边界处理。
4.1 图像读取与预处理
function enhanced = underwaterFusion(inputPath) % 读取图像并转为double类型,避免uint8运算溢出 img = imread(inputPath); img = im2double(img); % 如果输入是灰度图,先复制成三通道,保证统一处理 if size(img, 3) == 1 img = repmat(img, [1, 1, 3]); end % 可选的轻度去噪:中值滤波,核大小3x3 img = medfilt2(img(:,:,1), [3 3]); % 仅示意,实际要对三通道都处理 end一个常见的坑是Matlab里im2double之后图像范围是[0,1],但后续的某些滤波操作可能会产生超出这个范围的值,最后显示时要记得用imshow(I, [])或者clamp到[0,1],不然显示出来就是一片白或者一片黑,让你误以为算法写错了。
4.2 颜色恢复分支的实现
颜色恢复分支的核心是灰度世界校正:假设整张图的三通道均值应该均衡。但前面说了,水下红色通道天然偏弱,直接做灰度世界会过度校正,所以我会给红色通道的增益加一个上限。
function colorCorrected = colorRestoration(img, maxRedGain) meanR = mean2(img(:,:,1)); meanG = mean2(img(:,:,2)); meanB = mean2(img(:,:,3)); % 以绿色通道为基准 gainR = meanG / (meanR + eps); gainB = meanG / (meanB + eps); % 限制红色通道增益,避免过度校正 gainR = min(gainR, maxRedGain); corrected = img; corrected(:,:,1) = img(:,:,1) * gainR; corrected(:,:,2) = img(:,:,2); corrected(:,:,3) = img(:,:,3) * gainB; % 饱和度轻度拉伸,提升观感 hsv = rgb2hsv(corrected); hsv(:,:,2) = hsv(:,:,2) * 1.1; colorCorrected = hsv2rgb(hsv); endmaxRedGain这个参数我一般设在1.5到2.5之间。浅水素材红色信息还有残留,增益1.8左右比较稳;深水素材红色通道几乎全是噪声,增益上限设太大会把噪声也放大,这时1.3以下更安全。
4.3 细节增强分支的实现
细节分支我采用了两步处理:先对亮度通道做CLAHE,再用拉普拉斯金字塔提取高频信息回加。
function detailEnhanced = detailEnhancement(img, claheClipLimit) % 转到Lab空间,只处理亮度通道,避免颜色被进一步扭曲 lab = rgb2lab(img); L = lab(:,:,1); % CLAHE局部对比度增强 L_enhanced = adapthisteq(L, 'ClipLimit', claheClipLimit, ... 'NumTiles', [8 8], 'Distribution', 'rayleigh'); % 高频细节提取与回加 gaussianKernel = fspecial('gaussian', [5 5], 1.2); L_blur = imfilter(L_enhanced, gaussianKernel, 'replicate'); L_detail = L_enhanced - L_blur; detailEnhancedL = L_enhanced + 0.6 * L_detail; % 回写亮度通道 lab_enhanced = lab; lab_enhanced(:,:,1) = detailEnhancedL; detailEnhanced = lab2rgb(lab_enhanced); end这里有个小技巧:CLAHE的ClipLimit不要一上来就调大。ClipLimit太小改善不明显,但太大局部对比度会过冲,水下素材里的小颗粒悬浮物会被放大成明显的“雪花点”。我实测下来,水下视频帧的ClipLimit设置在0.015到0.03之间比较合适,宁小勿大。
4.4 主函数串联两个分支
function result = underwaterFusionPipeline(inputPath, params) img = im2double(imread(inputPath)); % 分支一:颜色恢复 branchColor = colorRestoration(img, params.maxRedGain); % 分支二:细节增强 branchDetail = detailEnhancement(img, params.claheClipLimit); % 计算各自权重图 weightColor = computeWeights(branchColor); weightDetail = computeWeights(branchDetail); % 归一化权重 sumWeight = weightColor + weightDetail + 1e-6; weightColorNorm = weightColor ./ sumWeight; weightDetailNorm = weightDetail ./ sumWeight; % 多尺度融合 result = multiscaleFusion(branchColor, branchDetail, ... weightColorNorm, weightDetailNorm); end整个管线在代码层面非常清晰,真正花时间的是调试权重和融合参数。别指望第一次跑通就能出片,我至少调了两周才让主观效果稳定下来。
5. 权重图与多尺度融合:像素级的“民主投票”机制
权重图是融合方案里最核心的部分,也是和“简单叠加”拉开差距的关键设计。这一节细说权重计算和融合的代码逻辑。
5.1 对比度、饱和度、曝光良好度的计算
三个权重标量在代码里逐像素计算:
function W = computeWeights(img) % 转灰度,计算对比度权重:局部标准差 gray = rgb2gray(img); localStd = stdfilt(gray, ones(5)); W_contrast = localStd; % 饱和度权重:RGB三通道标准差 W_saturation = std(img, 0, 3); % 曝光良好度权重:像素值距离中值0.5的远近 W_exposure = exp(-(gray - 0.5).^2 / (2 * 0.25^2)); % 三项相乘 W = W_contrast .* W_saturation .* W_exposure; % 平滑权重图,避免融合时出现块状边界 W = imgaussfilt(W, 2); end重点说两个容易被忽略的点。第一,对比度权重的窗口大小不是固定的,窗口太小容易受噪声干扰,窗口太大又模糊了权重图的局部性,5x5是一个安全和性能的折中。第二,权重图一定要平滑处理,不然融合后的图像会在边缘区域出现类似“过度锐化”的坏边,视觉效果非常突兀。
5.2 为什么融合要套拉普拉斯金字塔
简单的加权平均之所以不可行,还有一个视觉层面的原因:人眼对低频颜色变化和高频细节差异的敏感度不同。直接在原图上做加权叠加,会在明暗交替的区域产生伪影,也就是我们常说的“光晕”。多尺度融合的本质是把图像分解成不同频带,分别对每个频带做加权融合,然后再重建,这样既避免了光晕,也让细节和颜色各自在自己的频带上尽量干净。
Matlab里用金字塔融合有现成的思路:把每张候选图分解成拉普拉斯金字塔,把权重图分解成高斯金字塔,然后在每一层用权重金字塔对拉普拉斯金字塔做加权平均,最后从顶层逐级上采样相加得到结果。
5.3 金字塔融合代码骨架
function fused = multiscaleFusion(img1, img2, w1, w2, levels) % 对输入图像构建拉普拉斯金字塔 pyr1 = laplacianPyramid(img1, levels); pyr2 = laplacianPyramid(img2, levels); % 对权重图构建高斯金字塔 gpyr_w1 = gaussianPyramid(w1, levels); gpyr_w2 = gaussianPyramid(w2, levels); % 逐层加权融合 fusedPyr = cell(1, levels); for l = 1:levels sumW = gpyr_w1{l} + gpyr_w2{l} + 1e-6; fusedPyr{l} = (pyr1{l} .* gpyr_w1{l} + ... pyr2{l} .* gpyr_w2{l}) ./ sumW; end % 重建 fused = fusedPyr{levels}; for l = levels-1:-1:1 fused = imresize(fused, size(fusedPyr{l}(:,:,1))) + fusedPyr{l}; end end拉普拉斯金字塔层数我一般选5到6层。层数太少,低频颜色信息融合不充分,层数太多,最低频那一层几乎散布整张图的全局颜色,意义不大还会拖慢速度。Matlab里可以用impyramid函数做金字塔,但它一次只处理一层,多通道输入要分别处理再拼回来,写的时候注意别把通道维搞丢了。
5.4 融合结果的主观质量观察点
跑完融合之后,我会从三个维度快速评估主观效果:一是背景区域的颜色是否自然,不再有红色伪影或者死蓝;二是物体边缘是否清晰,没有光晕和过冲;三是暗部区域的噪声是否被压制,没有出现明显的颗粒感。三个维度都通过了,才敢把算法交给下一步的视频增强测试。
6. 从静态图像到视频:时序一致性与计算开销的取舍
融合算法在单帧上效果好,不代表直接在视频帧上逐帧跑也能直接交付。视频增强比图像增强多出来的挑战,几乎全在时序一致性上。
6.1 帧间闪烁:单帧增强的“阿喀琉斯之踵”
逐帧独立处理最典型的问题就是闪烁。因为每一帧的统计量(比如灰度世界均值、对比度权重分布)都在变化,算法对每一帧的增强强度也在微调,结果就是画面整体的亮度、颜色在时间维度上抖来抖去。静态看每一帧都觉得不错,一旦连起来播放,就能明显感到画面在“呼吸”,这是视频增强的大忌。
闪烁在水下视频里会被进一步放大,因为水下环境本身就有大量悬浮颗粒在移动,帧与帧之间的光照也在变化。如果增强算法对统计量敏感,一点小变化都会体现为画面抖动。
6.2 缓解闪烁的三个实用策略
我在项目里用了三个策略来控制闪烁。
第一,给统计量做时间平滑。比如计算灰度世界增益时,不再只依赖当前帧,而是对最近几帧的均值做一个指数移动平均。这个策略非常有效,代价几乎是零。
第二,参数联动而非参数固定。不要用完全固定的ClipLimit和增益上限,而是让它们随场景统计量缓慢变化。参量变化速度用时间常数控制,避免突变。
第三,关键帧增强+中间帧映射。对视频做滑窗,每N帧算一次增强参数,中间帧用线性插值得到渐变参数,这样可以保证帧间变化的连续性。N我取5到10帧,效果已经足够平滑。
6.3 Matlab做视频处理的性能瓶颈
Matlab做视频增强的性能瓶颈主要在循环。如果逐帧调用imread、adapthisteq、金字塔融合这一整套流程,一帧处理时间可能达到数百毫秒,完全达不到实时。我在项目里做了两个优化:
第一个优化是多帧并行。把视频读取改成按批读取,然后用parfor对帧做增强处理,再利用Matlab的Image Processing Toolbox对单帧操作本身的向量化程度就已经很高了,真正耗时的是金字塔循环,这一块可以用Matlab Coder生成C代码,实测速度能提升一个数量级。
第二个优化是降低冗余计算。视频帧之间内容高度相似,可以只对关键帧做完整的融合增强,中间帧用上一次的权重图做轻量处理,效果损失不大但速度翻倍。不过这个策略对剧烈运动的场景不友好,运动幅度大的时候还是得老老实实逐帧全量处理。
7. 实验评测:指标、判读与“看着好但指标差”的陷阱
算法做完了总要有个客观交代。但水下增强的评测非常容易翻车,因为绝大多数水下素材没有对应的“清晰原图”可以参考,你很难用PSNR这类全参考指标给出有说服力的结论。
7.1 两个可靠的无参考质量指标
水下图像质量评测,我常用的是UICM和UCIQE。UICM关注色彩平衡程度和色彩丰富度,UCIQE关注色偏、对比度和饱和度的综合水平。这两个指标不需要参考图,正好适配水下场景。
说一下实测规律。浅水素材经过融合算法后,UICM和UCIQE通常都有明显提升,因为颜色恢复分支大幅度拉高了色彩丰富度。深水素材的指标提升幅度会更小,因为红色通道信息实在缺失,强行提升指标只会放大噪声,指标涨了但观感更差,这时候需要人工判断优先保什么。
7.2 为什么全参考指标在水下不适用
有部分公开数据集是采集时同时拍了原始清晰版和退化版的,理论上可以用PSNR和SSIM。但水下退化不是线性的全局处理,而是空间变化的复杂过程,同一套退化参数在不同深度不同水质下差异很大。你在某个模拟退化集上测出的PSNR提升,换到真实水下视频上可能完全没有意义。
我的建议是:全参考指标在算法调试阶段用,用来确认“分量变化是否符合预期”,最终验收阶段看无参考指标+主观评估结果。主观评估最好找多个观察者独立打分,避免一个人看图产生幸存者偏差。
7.3 一个典型测试结果分析
我拿某段浅水浑浊视频做了测试,原图UICM大约0.35,UCIQE大约0.28,融合处理后UICM提升到0.58,UCIQE提升到0.42。主观上看颜色从灰绿色变成自然偏蓝的暖色,远处岩石的轮廓从“糊成一片”到“能分辨大致边界”,整体观感改善明显。这个结果说明,融合分支的两个“补丁”确实都在发挥作用。不过我也注意到,在某些极深场景,UCIQE提升有限,主要原因是蓝色通道全面主导,饱和度本身没有多少可提升空间了,“补丁”也只能在有限范围内起作用。
8. 真实项目的落地调整:参数初始化、颜色偏好与性能优化
最后聊聊这套算法在实际项目里“从能跑到好用”的距离。算法原型是一回事,参数调优和工程化又是另一回事。
8.1 参数初始化的推荐模板
我给出一组在多个场景下表现稳定的初始参数,可以作为调优起点:
| 参数 | 推荐初始值 | 调节方向 |
|---|---|---|
| maxRedGain | 1.8 | 浅水可增至2.2,深水降至1.2 |
| CLAHE ClipLimit | 0.02 | 细节不足增大,噪点明显减小 |
| 高频回加强度 | 0.6 | 边缘发虚增大,过冲减小 |
| 权重对比度窗口 | 5x5 | 噪声大时增大窗口 |
| 金字塔层数 | 5 | 分辨率高可增至6 |
| 视频平滑时间常数 | 0.3 | 闪烁明显减小,拖影增大时变大 |
这套参数不是拍脑袋定的,是从模拟项目X的大量水槽测试和不同水深素材里反向拟合出来的。建议不要同时调所有参数,先固定颜色分支,调细节分支,再反过来调颜色分支,一次只动一个旋钮,不然会陷入调参地狱。
8.2 颜色偏好的工程化选项
水下视频用户对颜色的要求其实是两极分化的。一类用户要“自然还原”,恨不得把深水里的蓝绿全变成陆地色调;另一类用户要“保真”,他们认为水下就应该是蓝色调,过度去蓝反而失真。这两个需求不能靠一套固定参数满足,我最后做成了两个预设模式:自然模式用低频颜色增益,深度还原模式用更强的白平衡分支权重。
这个决策听起来像是产品经理的需求,但它直接影响算法参数的约束范围。比如在“保真”模式下,maxRedGain会被限制在1.2以下,颜色分支的融合权重也被压低,以避免产生明显的去蓝效果。提前在算法层留出这个开关,比后期用后处理滤镜去调整要稳定得多。
8.3 性能优化和部署的一些经验
原型是Matlab代码,交付阶段我用Matlab Coder转成了C代码,整个增强管线从一个分支的最小函数开始逐步替换验证,避免大范围重构导致回归错误。转换之后帧率大概从0.5帧/秒提升到8帧/秒左右,满足了离线后处理需求。如果对实时性有更高要求,还有几个方向可以优化:一是把金字塔层数从5降到3;二是用缩略图计算权重图,再上采样用于融合,权重图分辨率不用和原图一样;三是把RGB空间处理改成YCbCr空间,只对亮度通道做高频细节增强,彩色通道只做轻量颜色校正,运算量能省不少。
关于工程化还有一个细节:不同的ROV或者水下相机会有自带的色彩配置文件,进到算法前要确保颜色空间是一致的。我在项目里遇到过同型号设备两套固件输出色彩不一致的问题,导致算法参数在一个设备上调好,换另一台就效果崩坏。解决方案是加一个极简的色彩校准步骤,用标准白板在校验场景拍一帧,计算出三通道校正系数,再和增强管线串联起来。
水下图像增强这个方向,算法堆叠再多也不如物理理解到位。融合算法本质上是把“不知道水下退化精确模型”这件事的负面影响降到最低,承认单一算法不行,才愿意用多分支加权重的方式去兜底。如果你也在做类似项目,我建议先把退化模型和分支设计看懂,再动手写代码,这也是我这次项目完成之后最深的体会。