4. 经典去雾算法(二):基于Retinex理论的去雾方法、多尺度Retinex(MSR)与带色彩恢复的MSR(MSRCR)
上一章我们聊了暗通道先验,今天来聊聊另一条技术路线——Retinex理论。
说实话,我第一次接触Retinex是在做图像增强项目时。当时甲方要求「把雾天拍的监控画面变清晰」,我试了直方图均衡化,效果惨不忍睹。后来翻到一篇老,才发现Retinex才是正解。
Retinex这个词,是Retina(视网膜)和Cortex(大脑皮层)的合成词。它的核心思想很简单:人眼看到的颜色,取决于物体本身的反射率,而不是光照强度。
你想想看,一张白纸在阳光下和阴影里,我们依然觉得它是白的。为什么?因为大脑自动「去掉」了光照的影响。Retinex算法就是模拟这个机制。
4.1 Retinex理论的核心思想
Retinex把一张图像分解成两部分:
- 入射光分量(Illumination):环境光照,变化缓慢,属于低频信息
- 反射分量(Reflectance):物体本身特性,包含细节和纹理,属于高频信息
用公式表达就是:
S(x,y) = R(x,y) · I(x,y)
其中S是观测到的图像,R是反射分量,I是入射光分量。去雾的本质,就是从S中分离出R,去掉光照I的影响。
关键理解:去雾不是「去掉雾」,而是「恢复反射分量」。雾相当于一个不均匀的光照层,Retinex把它当作I来处理。
实际操作中,我们通常把公式转到对数域:
log(S) = log(R) + log(I)
为什么取对数?两个原因:一是把乘法变加法,方便分离;二是更符合人眼对亮度的非线性感知。
4.2 单尺度Retinex(SSR)
SSR是最基础的版本。它的做法是:用高斯滤波估计光照分量I,然后用原图减去I,得到R。
步骤很简单:
- 对原图S做高斯模糊,得到光照估计I
- 在对数域计算:log(R) = log(S) - log(I)
- 取指数,得到反射分量R
- 做线性拉伸,映射到0-255
代码实现也很直接:
import cv2
import numpy as np
def SSR(img, sigma=80):
# 转浮点,加1避免log(0)
img = np.float32(img) + 1.0
# 高斯模糊估计光照
I = cv2.GaussianBlur(img, (0, 0), sigma)
# 对数域相减
R = np.log(img) - np.log(I)
# 线性拉伸到0-255
R = cv2.normalize(R, None, 0, 255, cv2.NORM_MINMAX)
return np.uint8(R)
经验之谈:sigma参数很关键。sigma太小,保留太多光照信息,去雾不彻底;sigma太大,细节被过度平滑,图像发灰。我一般从80开始调,根据雾的浓度上下浮动。
SSR的问题很明显:单尺度无法兼顾全局和局部。小尺度能保留细节,但颜色失真严重;大尺度颜色自然,但细节丢失。怎么办?多尺度融合。
4.3 多尺度Retinex(MSR)
MSR的思路很朴素:用多个不同尺度的高斯滤波,分别做SSR,然后加权平均。
公式:
R_msr = Σ w_i · R_i
其中R_i是第i个尺度下的SSR结果,w_i是权重,通常各取1/3。
我习惯用三个尺度:小(15)、中(80)、大(250)。小尺度抓细节,大尺度保颜色,中尺度做过渡。
def MSR(img, sigmas=[15, 80, 250]):
img = np.float32(img) + 1.0
R_sum = np.zeros_like(img)
for sigma in sigmas:
I = cv2.GaussianBlur(img, (0, 0), sigma)
R = np.log(img) - np.log(I)
R_sum += R
R_msr = R_sum / len(sigmas)
R_msr = cv2.normalize(R_msr, None, 0, 255, cv2.NORM_MINMAX)
return np.uint8(R_msr)
效果比SSR好很多,但有个新问题:颜色偏灰。尤其是彩色图像,看起来像蒙了一层灰纱。
注意:MSR对RGB三个通道独立处理,破坏了通道间的比例关系,导致颜色偏移。这不是算法bug,而是原理缺陷。
4.4 带色彩恢复的MSR(MSRCR)
MSRCR在MSR基础上加了一个色彩恢复因子C,用来修正颜色偏移。
核心公式:
R_msrcr = C_i · R_msr_i
其中色彩恢复因子C_i的计算方式:
C_i = β · log(α · I_i / Σ I_j)
参数说明:
| 参数 | 含义 | 推荐值 |
|---|---|---|
| β | 增益系数 | 46 |
| α | 调节非线性 | 125 |
| I_i | 第i通道的像素值 | - |
实现代码:
def MSRCR(img, sigmas=[15, 80, 250], alpha=125, beta=46):
img_float = np.float32(img) + 1.0
# 先做MSR
R_msr = np.zeros_like(img_float)
for sigma in sigmas:
I = cv2.GaussianBlur(img_float, (0, 0), sigma)
R = np.log(img_float) - np.log(I)
R_msr += R
R_msr /= len(sigmas)
# 计算色彩恢复因子
sum_I = np.sum(img_float, axis=2, keepdims=True)
C = beta * (np.log(alpha * img_float) - np.log(sum_I))
# 应用色彩恢复
R_msrcr = C * R_msr
# 拉伸到0-255
R_msrcr = cv2.normalize(R_msrcr, None, 0, 255, cv2.NORM_MINMAX)
return np.uint8(R_msrcr)
效果对比:MSRCR在保持细节的同时,颜色还原明显优于MSR。但计算量也大了不少,实时处理有压力。
4.5 三种方法的对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SSR | 实现简单,速度快 | 单尺度顾此失彼,颜色失真 | 快速原型验证 |
| MSR | 细节丰富,动态范围大 | 颜色偏灰,需要后处理 | 灰度图增强、监控视频 |
| MSRCR | 颜色自然,综合效果好 | 计算量大,参数调优麻烦 | 高质量图像去雾、摄影后期 |
4.6 核心流程总结
下面这张图概括了Retinex去雾的完整流程:
4.7 项目中的避坑指南
我在实际项目中踩过不少坑,分享几个最典型的:
- 颜色过饱和:MSRCR的α和β参数别照搬值。我遇到过α=125时红色通道溢出,降到80才正常。建议根据实际图像微调。
- 光晕效应:高斯滤波的核大小要选奇数。我曾经用(0,0)自动计算,结果边缘出现奇怪的光晕,改成手动指定就好了。
- 噪声放大:Retinex在暗区会放大噪声。我的做法是在对数变换前加一个小阈值,比如img = np.maximum(img, 1e-6),避免log(0)和噪声放大。
- 实时性优化:MSRCR在1080p图像上要跑几百毫秒。如果做视频去雾,建议降采样到640x360处理,再上采样回原尺寸,肉眼几乎看不出差别。
我的建议:如果追求效果,用MSRCR;如果追求速度,用MSR+简单的颜色校正(比如直方图匹配)。没有银弹,只有取舍。