4. 传统增强方法(二):伽马校正、对数变换、Retinex理论(SSR/MSR/MSRCR)

上一章我们聊了直方图均衡化那套东西。这一章,咱们继续深入几个更经典的传统方法。说实话,这些方法虽然“传统”,但在很多场景下,尤其是你手头算力有限、或者需要快速出效果的时候,它们依然非常能打。

我个人习惯把伽马校正和对数变换归为一类——它们都是像素级的非线性映射。而Retinex理论,则是另一套完全不同的思路,它试图从物理模型上把“光照”和“物体本身颜色”拆开。嗯,咱们一个一个来看。

4.1 伽马校正:不只是显示器的专利

很多人一听到伽马校正,第一反应是“哦,显示器用的”。没错,CRT显示器的亮度响应确实是非线性的,但伽马校正本身是一个极其强大的工具,用来调整图像的整体亮度感知。

它的公式很简单:

O = I ^ (1 / γ)

其中 I 是归一化到 [0,1] 的输入像素值,O 是输出,γ 是伽马值。

  • γ < 1:比如 γ=0.4,相当于对暗部进行拉伸,亮部压缩。图像整体变亮,暗部细节更明显。
  • γ > 1:比如 γ=2.2,相当于对亮部进行拉伸,暗部压缩。图像整体变暗,对比度看起来更高。
  • γ = 1:线性映射,啥也不干。
我的经验: 在低照度图像增强中,我通常把 γ 设置在 0.3 到 0.6 之间。但要注意,γ 太小会让图像看起来“发灰”,像蒙了一层雾。这时候就需要配合对比度拉伸或者后面要讲的 Retinex 来补救。

你可能会问:“为什么不用线性拉伸?” 因为人眼对亮度的感知本身就是非线性的。伽马校正更符合人眼的视觉特性,处理后的图像看起来更“自然”。

4.1.1 代码实现

import cv2
import numpy as np

def gamma_correction(img, gamma=0.5):
    # 归一化到 [0,1]
    img_norm = img / 255.0
    # 伽马校正
    img_gamma = np.power(img_norm, 1.0 / gamma)
    # 还原到 [0,255]
    img_gamma = np.clip(img_gamma * 255, 0, 255).astype(np.uint8)
    return img_gamma

# 读取图像
img = cv2.imread('low_light.jpg')
# 应用伽马校正
result = gamma_correction(img, gamma=0.4)
cv2.imshow('Gamma Corrected', result)
cv2.waitKey(0)
避坑指南: 我曾经在项目中直接对整张图做伽马校正,结果发现天空部分过曝严重。后来我意识到,伽马校正是全局操作,它不会区分“暗部”和“亮部”。如果图像动态范围很大,建议先做局部处理,或者结合后面的 Retinex 方法。

4.2 对数变换:压缩动态范围的利器

对数变换和伽马校正有点像,但它的数学形式是:

O = c * log(1 + I)

它的核心作用是压缩高动态范围。你看,对数函数在输入值较小时斜率大(拉伸暗部),在输入值较大时斜率小(压缩亮部)。

这在处理大光比场景时特别有用。比如你拍了一张夜景,路灯很亮,但阴影处一片漆黑。对数变换能把路灯的亮度压下来,同时把阴影里的细节提起来。

关键区别:
  • 伽马校正:通过指数调整,改变整体亮度感知。
  • 对数变换:通过对数压缩,缩小动态范围,特别适合高动态场景。

4.2.1 代码实现

def log_transform(img, c=1.0):
    # 归一化
    img_norm = img / 255.0
    # 对数变换
    img_log = c * np.log(1 + img_norm)
    # 归一化到 [0,255]
    img_log = np.clip(img_log * 255, 0, 255).astype(np.uint8)
    return img_log

result_log = log_transform(img, c=1.2)

我个人习惯把对数变换用在预处理阶段。比如在做 3A 算法联动调优时,如果 AE(自动曝光)给出的图像整体偏暗,我会先用对数变换把暗部细节拉出来,再交给后面的 AWB(自动白平衡)和 AF(自动对焦)去处理。效果比直接调曝光参数要稳定得多。

4.3 Retinex理论:把光照和反射拆开

好了,前面两个方法都是“像素级”的操作。Retinex 理论则完全不同,它试图从物理模型出发,把图像分解成两部分:

  • 光照分量 (Illumination):场景中的光源分布,通常是低频的、平滑的。
  • 反射分量 (Reflectance):物体本身的颜色和纹理,是高频的、细节丰富的。

公式很简单:

S = R * L

其中 S 是观察到的图像,R 是反射分量,L 是光照分量。我们的目标就是估计出 L,然后去掉它,得到 R。这样就能消除光照不均的影响,把物体本来的颜色还原出来。

你想想看,这个思路是不是很优雅?它不直接去“增强”像素,而是去“恢复”物理本质。

4.3.1 SSR:单尺度Retinex

SSR(Single Scale Retinex)是最基础的版本。它的做法是:

  1. 对图像做高斯模糊,得到光照分量 L 的估计。
  2. 在 log 域中,用原图减去 L,得到反射分量 R。
  3. 对 R 做指数变换,还原到正常范围。

公式:

R(x,y) = log(S(x,y)) - log(G(x,y) * S(x,y))

其中 G 是高斯核,* 是卷积操作。

我的经验: 高斯核的尺度(sigma)是关键参数。sigma 太小,光照估计不准确,图像会出现“光晕”伪影;sigma 太大,细节丢失严重。我一般从 15 到 80 之间试,具体取决于图像分辨率。

4.3.2 MSR:多尺度Retinex

SSR 的问题在于,单一尺度无法同时兼顾细节和色彩。于是有了 MSR(Multi-Scale Retinex)。它把多个不同尺度的 SSR 结果加权平均:

R_MSR = Σ w_i * R_i

其中 R_i 是第 i 个尺度的 SSR 结果,w_i 是权重,通常取等权。

这样做的好处是:小尺度保留细节,大尺度保证色彩自然。我一般在项目中用 3 个尺度:小(sigma=15)、中(sigma=50)、大(sigma=120)。

4.3.3 MSRCR:带色彩恢复的多尺度Retinex

MSR 有一个明显的缺点:色彩失真。因为它在三个通道上独立处理,破坏了通道间的比例关系。MSRCR(Multi-Scale Retinex with Color Restoration)就是为了解决这个问题。

它引入了一个色彩恢复因子 C:

R_MSRCR(x,y) = C(x,y) * R_MSR(x,y)

其中 C 的计算依赖于原图各通道的比值。说白了,就是让处理后的图像尽量保持原图的色彩比例。

三种Retinex对比:
方法 核心思想 优点 缺点
SSR 单尺度高斯模糊估计光照 实现简单,速度快 单一尺度无法兼顾细节和色彩
MSR 多尺度加权平均 细节和色彩平衡较好 色彩容易失真
MSRCR MSR + 色彩恢复因子 色彩还原度高,效果最自然 计算量大,参数调优复杂

4.3.4 代码实现

def ssr(img, sigma=50):
    img_log = np.log1p(img.astype(np.float32))
    blur = cv2.GaussianBlur(img_log, (0, 0), sigma)
    r = img_log - blur
    r = np.exp(r) - 1
    r = np.clip(r, 0, 255).astype(np.uint8)
    return r

def msr(img, sigmas=[15, 50, 120]):
    img_float = img.astype(np.float32)
    result = np.zeros_like(img_float)
    for sigma in sigmas:
        result += ssr(img_float, sigma)
    result /= len(sigmas)
    return np.clip(result, 0, 255).astype(np.uint8)

def msrcr(img, sigmas=[15, 50, 120], alpha=125, beta=46, G=192, b=-30):
    img_float = img.astype(np.float32)
    img_log = np.log1p(img_float)
    
    # MSR
    msr_result = np.zeros_like(img_float)
    for sigma in sigmas:
        blur = cv2.GaussianBlur(img_log, (0, 0), sigma)
        msr_result += img_log - blur
    msr_result /= len(sigmas)
    
    # 色彩恢复
    sum_channels = np.sum(img_float, axis=2, keepdims=True) + 1e-6
    C = beta * (np.log(alpha * img_float + 1) - np.log(alpha * sum_channels + 1))
    
    # 最终结果
    result = G * (C * msr_result + b)
    result = np.clip(result, 0, 255).astype(np.uint8)
    return result
避坑指南: 我曾经在 MSRCR 的参数调优上花了两周时间。alpha、beta、G、b 这四个参数对最终效果影响极大,而且不同场景需要不同的参数组合。我的建议是:先用默认参数跑一遍,然后根据输出图像的“偏色”和“亮度”情况,手动微调 G 和 b。G 控制整体增益,b 控制亮度偏移。

4.4 本章知识体系

下面这张图总结了本章的核心逻辑,从像素级操作到物理模型,层层递进:

传统低照度增强方法体系 像素级非线性映射 物理模型:Retinex理论 伽马校正 对数变换 SSR MSR MSRCR(带色彩恢复) 核心思路对比: • 伽马/对数:直接操作像素值,不考虑物理成因,速度快但效果有限。 • Retinex:从 S = R * L 模型出发,分离光照和反射,效果更自然。 • MSRCR:在 MSR 基础上加入色彩恢复,解决偏色问题,但参数调优复杂。 • 实际项目中,我常把伽马校正作为 Retinex 的后处理,用来微调最终亮度。

4.5 本章小结

这一章我们聊了三种经典方法:

  • 伽马校正:简单粗暴,适合快速提亮暗部,但要注意过曝问题。
  • 对数变换:压缩动态范围,适合大光比场景,我常用在预处理阶段。
  • Retinex理论:从物理模型出发,SSR/MSR/MSRCR 层层递进,效果越来越好,但计算量和参数复杂度也在增加。