水下图像增强这个方向,我从几年前做水下机器人视觉项目时就开始关注了。当时最头疼的问题就是:同一套目标检测模型,在空气中跑得好好的,一到水下就各种漏检误检,画面偏蓝偏绿、对比度低、远处雾蒙蒙一片。后来翻了不少论文,其中WCID这篇算是比较早把“波长补偿”和“图像去雾”两条路线合在一起的工作,思路清晰,工程上也好落地。今天就把我对这篇论文的理解、复现过程、踩过的坑,以及后续做的一些改进,完整地聊一聊。
如果你正在做水下视觉、水下机器人、水下摄影后期,或者单纯对图像增强感兴趣,这篇内容应该能给你一些可以直接抄作业的东西。我会从原理讲到代码实现,再到参数调试和问题排查,尽量把每个“为什么”都说清楚。
1. 水下图像为什么会变成这样
1.1 光在水里到底经历了什么
要理解WCID为什么这么设计,得先搞清楚水下成像的物理过程。光在空气中传播,衰减很慢,几百米外的东西还能看清轮廓。但到了水里,情况完全不一样。
水对不同波长的光吸收率差异极大。红光波长最长,最先被吸收,大概在水下5米左右就基本消失了;橙光、黄光紧随其后;蓝光和绿光穿透力最强,能到几十米。这就是为什么大部分水下照片都是蓝绿色调——不是相机白平衡坏了,是红光真的没了。
除了吸收,还有散射。水里的悬浮颗粒、浮游生物会把光线向各个方向散射,导致两个后果:一是画面整体蒙上一层“雾”,对比度下降;二是前向散射会让边缘模糊,细节丢失。距离越远,这两个效应越明显。
用一个简化的物理模型来描述:
I(x) = J(x) · t(x) + A · (1 - t(x))
其中I是相机拍到的图像,J是真实场景的反射光(也就是我们想恢复的),t是透射率,A是背景光/水体光。这个模型和大气去雾的模型形式一样,但水下多了“波长相关衰减”这一层。
1.2 为什么不能直接套用大气去雾
很多人第一反应是:既然模型一样,那直接拿暗通道先验去雾不就行了?我一开始也是这么想的,实测下来效果并不理想。
原因在于,大气去雾假设三个通道的衰减是一致的,但水下红通道衰减远快于蓝绿通道。如果直接去雾,暗通道估计出来的透射率对红通道是偏大的,恢复出来的红色要么过曝要么噪声爆炸。另外,水下背景光A通常不是灰白色,而是偏蓝绿,直接估计会引入色偏。
WCID的核心贡献,就是在去雾之前先做一步“波长补偿”,把红通道的能量补回来,让三个通道的衰减尽量拉平,再去雾就合理多了。
1.3 WCID的整体思路一句话概括
先用波长补偿把红通道的衰减补上,再用改进的去雾算法去除散射造成的雾化,两步走。听起来简单,但每一步都有不少细节。
2. 波长补偿这一步到底怎么做的
2.1 补偿的本质是“借能量”
红通道在水下衰减最严重,但它的信息并不是完全消失了,而是被压缩了。波长补偿的思路,就是从蓝绿通道里“借”一部分能量,按一定比例补偿到红通道。
论文里用的方法,核心是分析蓝绿通道和红通道之间的关系。具体来说,它假设在局部区域内,红通道和蓝绿通道的强度存在某种线性或非线性映射。通过统计整幅图像的通道均值、方差等,估计出一个补偿系数。
我复现时用的简化版本是这样的:
import cv2 import numpy as np def wavelength_compensation(img, alpha=1.0, beta=0.5): """ img: BGR格式 alpha: 红通道补偿强度 beta: 蓝绿通道借用比例 """ b, g, r = cv2.split(img.astype(np.float32)) # 计算蓝绿通道的平均值作为补偿源 bg_mean = (b + g) / 2.0 # 红通道补偿:原始红 + 从蓝绿借来的能量 r_compensated = r + alpha * (bg_mean - r) * beta # 限制范围 r_compensated = np.clip(r_compensated, 0, 255) # 合并 result = cv2.merge([b, g, r_compensated.astype(np.uint8)]) return result这段代码里,alpha控制补偿的总强度,beta控制从蓝绿通道借多少。实测下来,alpha在0.8到1.2之间、beta在0.3到0.6之间比较稳。
2.2 补偿系数不是拍脑袋定的
上面代码里的参数看着简单,但怎么定是有讲究的。论文里通过分析大量水下图像,发现红通道的衰减程度和场景深度、水体浊度相关。深度越大、水越浑,红通道损失越严重,补偿就要越强。
我的做法是:先算整幅图像红通道的均值,再算蓝绿通道均值的平均,两者比值作为衰减指标。比值越小,说明红通道损失越严重,alpha就取大一点。
def estimate_alpha(img): b, g, r = cv2.split(img.astype(np.float32)) r_mean = np.mean(r) bg_mean = (np.mean(b) + np.mean(g)) / 2.0 ratio = r_mean / (bg_mean + 1e-6) # ratio越小,衰减越严重,alpha越大 alpha = np.clip(1.5 - ratio, 0.5, 1.5) return alpha这个自适应策略比固定参数效果好不少,尤其是在处理不同深度的图像时。
2.3 补偿过度的后果
这里要特别提醒:补偿不是越多越好。我踩过的坑是,alpha设太大,红通道直接过曝,画面出现大片红色噪点,尤其是暗部区域。原因是暗部本来信噪比就低,强行放大红通道等于把噪声也放大了。
注意:补偿后一定要检查红通道直方图,如果出现明显的右侧堆积(大量像素接近255),说明补偿过度,需要调小alpha或beta。
另一个技巧是,补偿只在红通道上进行,蓝绿通道保持不动。有些改进版本会同时对蓝绿通道做轻微抑制,但WCID原版没有这一步,我建议先按原版来,效果不够再考虑。
3. 去雾环节的改进与实现
3.1 暗通道先验在水下的适配
去雾这一步,WCID用的是基于暗通道先验的框架,但做了水下适配。暗通道先验的核心观察是:无雾图像的局部区域里,总有一个通道的强度非常低(接近0)。水下图像经过波长补偿后,这个假设在一定程度上也成立。
暗通道的计算:
def dark_channel(img, patch_size=15): b, g, r = cv2.split(img.astype(np.float32)) min_channel = np.minimum(np.minimum(b, g), r) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (patch_size, patch_size)) dark = cv2.erode(min_channel, kernel) return darkpatch_size的选择有讲究。太小,暗通道估计噪声大;太大,透射率图会过于平滑,丢失细节。我一般从15开始试,图像分辨率高的话可以适当加大到21或31。
3.2 背景光估计的坑
背景光A的估计,传统做法是取暗通道最亮的前0.1%像素,在原图中找对应位置最亮的点。但水下图像有个问题:画面里最亮的区域往往是水体本身或者反光的悬浮颗粒,不是真正的背景光。
我的改进做法是:先排除图像边缘区域(边缘经常有水面反光),再在暗通道最亮的候选点里,选蓝绿通道均值最大的点作为背景光。因为水下背景光偏蓝绿,这个策略更稳。
def estimate_atmospheric_light(img, dark, top_percent=0.001): h, w = img.shape[:2] # 排除边缘 margin = int(min(h, w) * 0.1) mask = np.ones((h, w), dtype=bool) mask[:margin, :] = False mask[-margin:, :] = False mask[:, :margin] = False mask[:, -margin:] = False dark_masked = dark.copy() dark_masked[~mask] = 0 num_pixels = int(h * w * top_percent) flat_dark = dark_masked.flatten() indices = np.argsort(flat_dark)[-num_pixels:] flat_img = img.reshape(-1, 3) candidates = flat_img[indices] # 选蓝绿通道均值最大的 bg_score = candidates[:, 0] + candidates[:, 1] best_idx = np.argmax(bg_score) A = candidates[best_idx] return A3.3 透射率估计与细化
透射率t的估计,用的是经典公式:
t(x) = 1 - omega * min_over_channels( I(x) / A )
omega是去雾强度,一般取0.85到0.95。水下我建议取0.9左右,太低去雾不彻底,太高容易过增强。
直接算出来的透射率图会有块状效应,需要用导向滤波或软抠图细化。导向滤波速度快,效果也够用:
def refine_transmission(img, transmission, radius=40, eps=1e-3): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0 transmission = transmission.astype(np.float32) refined = cv2.ximgproc.guidedFilter(gray, transmission, radius, eps) return refined注意:
cv2.ximgproc需要opencv-contrib-python包,普通opencv-python没有这个模块。安装命令是pip install opencv-contrib-python。
3.4 恢复公式与参数微调
最后一步恢复:
J(x) = (I(x) - A) / max(t(x), t0) + A
t0是透射率下限,防止除零和噪声放大,一般取0.1。水下我建议取0.15到0.2,因为水下噪声本来就大,透射率太低时恢复出来的像素噪声会非常明显。
def recover(img, A, transmission, t0=0.15): img = img.astype(np.float32) A = A.astype(np.float32) transmission = np.maximum(transmission, t0) result = np.zeros_like(img) for i in range(3): result[:, :, i] = (img[:, :, i] - A[i]) / transmission + A[i] result = np.clip(result, 0, 255) return result.astype(np.uint8)4. 完整流程串起来与实测效果
4.1 从原图到增强图的完整代码
把上面几步串起来,就是一个完整的WCID流程:
def wcid_enhance(img, patch_size=15, omega=0.9, t0=0.15): # Step 1: 波长补偿 alpha = estimate_alpha(img) compensated = wavelength_compensation(img, alpha=alpha, beta=0.5) # Step 2: 暗通道 dark = dark_channel(compensated, patch_size) # Step 3: 背景光估计 A = estimate_atmospheric_light(compensated, dark) # Step 4: 透射率估计 norm_img = compensated.astype(np.float32) / A transmission = 1 - omega * dark_channel(norm_img, patch_size) # Step 5: 透射率细化 transmission = refine_transmission(compensated, transmission) # Step 6: 恢复 result = recover(compensated, A, transmission, t0) return result4.2 参数速查表
调参是复现论文最耗时间的环节。我把常用参数范围整理成表,方便你直接对照:
| 参数 | 含义 | 推荐范围 | 说明 |
|---|---|---|---|
| alpha | 红通道补偿强度 | 0.8-1.2 | 自适应估计更稳 |
| beta | 蓝绿借用比例 | 0.3-0.6 | 太大红通道会过曝 |
| patch_size | 暗通道窗口 | 15-31 | 高分辨率取大值 |
| omega | 去雾强度 | 0.85-0.95 | 水下建议0.9 |
| t0 | 透射率下限 | 0.15-0.2 | 防噪声放大 |
| radius | 导向滤波半径 | 30-60 | 越大越平滑 |
| eps | 导向滤波正则 | 1e-3-1e-2 | 越大越平滑 |
4.3 实测对比与主观评价
我拿几组不同场景的水下图像做了测试:浅水区珊瑚、深水区沉船、浑浊水域管道。整体来看,WCID对偏蓝绿、雾化明显的图像效果最好,色彩恢复自然,对比度提升明显。
但也有局限。对于红通道信息几乎完全丢失的深水图像,补偿出来的红色偏灰,不够鲜艳。这时候可以考虑在WCID之后再加一步白平衡或色彩迁移。另外,如果图像本身噪声很大,去雾后噪声会被放大,建议先去噪再增强。
5. 常见问题与排查技巧
5.1 增强后画面偏红或偏蓝
这是最常见的色彩问题。偏红通常是波长补偿过度,调小alpha或beta。偏蓝则是去雾不彻底或背景光估计偏蓝,可以适当提高omega,或者检查背景光估计是否选到了水体区域。
我的经验是,先固定去雾参数,单独调补偿参数,看红通道直方图是否合理;再固定补偿参数,调去雾参数。分开调比一起调容易定位问题。
5.2 暗部噪声爆炸
去雾公式里除以透射率,透射率小的地方噪声会被放大。解决办法有三个:提高t0、先去噪再增强、或者对暗部做单独的降噪处理。我一般用t0=0.2配合双边滤波,效果比较平衡。
5.3 透射率图出现块状或光晕
这是暗通道窗口太大或细化不够导致的。调小patch_size,或者增大导向滤波的radius和eps。如果还有光晕,可以试试软抠图,但速度会慢很多。
5.4 处理速度太慢
WCID的每一步都有优化空间。暗通道可以用积分图加速,导向滤波本身已经是线性复杂度,恢复步骤可以向量化。如果做视频处理,建议把图像缩放到720p以下再处理,速度能提升好几倍,效果损失不大。
5.5 问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 画面偏红 | 补偿过度 | 调小alpha/beta |
| 画面偏蓝 | 去雾不足 | 提高omega |
| 暗部噪声大 | 透射率过低 | 提高t0或先去噪 |
| 块状效应 | 窗口过大 | 调小patch_size |
| 光晕明显 | 细化不足 | 增大radius/eps |
| 处理慢 | 分辨率高 | 降采样或加速 |
6. 后续改进方向与我的一些实践
6.1 从WCID到深度学习
WCID是传统方法的代表,优点是物理可解释、不需要训练数据、参数少。但它的上限也明显:补偿和去雾都是基于先验假设,遇到复杂场景容易失效。
后来我试过用GAN做水下增强,效果确实更好,尤其是色彩和细节。但训练数据难搞,而且容易过拟合到特定水域。我的建议是:如果只是做少量图像处理,WCID够用;如果要做产品级应用,可以考虑WCID做预处理,再接一个轻量级网络做精修。
6.2 多尺度融合的思路
另一个改进方向是多尺度融合。水下图像的不同区域退化程度不一样,近处清晰、远处雾化。可以对图像分区域处理,近处少增强、远处多增强,再融合。这个思路在一些后续论文里有体现,实测对深水图像效果提升明显。
6.3 工程落地的一些体会
最后分享几个工程上的经验。第一,参数不要写死,要根据图像统计量自适应,否则换个场景就得重调。第二,处理流程要模块化,补偿、去雾、后处理分开,方便单独替换和调试。第三,一定要做主观评价,PSNR、SSIM这些指标在水下图像上参考价值有限,人眼看着舒服才是真的舒服。
我在实际项目里,最终上线的方案是WCID加一个轻量级的色彩校正网络,处理一帧1080p图像大概30毫秒,能满足实时性要求。踩过的坑主要是参数自适应和噪声控制,这两块做好了,WCID的稳定性还是很不错的。