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。

步骤很简单:

  1. 对原图S做高斯模糊,得到光照估计I
  2. 在对数域计算:log(R) = log(S) - log(I)
  3. 取指数,得到反射分量R
  4. 做线性拉伸,映射到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去雾的完整流程:

Retinex去雾算法核心流程 输入雾图 S(x,y) 对数变换 log(S) SSR:单尺度高斯 MSR:多尺度加权 MSRCR:+色彩恢复 指数变换 + 线性拉伸 输出去雾图 R(x,y) 核心思想:去掉光照分量I 保留反射分量R(物体本色)

4.7 项目中的避坑指南

我在实际项目中踩过不少坑,分享几个最典型的:

  • 颜色过饱和:MSRCR的α和β参数别照搬值。我遇到过α=125时红色通道溢出,降到80才正常。建议根据实际图像微调。
  • 光晕效应:高斯滤波的核大小要选奇数。我曾经用(0,0)自动计算,结果边缘出现奇怪的光晕,改成手动指定就好了。
  • 噪声放大:Retinex在暗区会放大噪声。我的做法是在对数变换前加一个小阈值,比如img = np.maximum(img, 1e-6),避免log(0)和噪声放大。
  • 实时性优化:MSRCR在1080p图像上要跑几百毫秒。如果做视频去雾,建议降采样到640x360处理,再上采样回原尺寸,肉眼几乎看不出差别。

我的建议:如果追求效果,用MSRCR;如果追求速度,用MSR+简单的颜色校正(比如直方图匹配)。没有银弹,只有取舍。