4. 传统增强方法(二):伽马校正、对数变换、Retinex理论(SSR/MSR/MSRCR)
上一章我们聊了直方图均衡化那套东西。这一章,咱们继续深入几个更经典的传统方法。说实话,这些方法虽然“传统”,但在很多场景下,尤其是你手头算力有限、或者需要快速出效果的时候,它们依然非常能打。
我个人习惯把伽马校正和对数变换归为一类——它们都是像素级的非线性映射。而Retinex理论,则是另一套完全不同的思路,它试图从物理模型上把“光照”和“物体本身颜色”拆开。嗯,咱们一个一个来看。
4.1 伽马校正:不只是显示器的专利
很多人一听到伽马校正,第一反应是“哦,显示器用的”。没错,CRT显示器的亮度响应确实是非线性的,但伽马校正本身是一个极其强大的工具,用来调整图像的整体亮度感知。
它的公式很简单:
O = I ^ (1 / γ)
其中 I 是归一化到 [0,1] 的输入像素值,O 是输出,γ 是伽马值。
- γ < 1:比如 γ=0.4,相当于对暗部进行拉伸,亮部压缩。图像整体变亮,暗部细节更明显。
- γ > 1:比如 γ=2.2,相当于对亮部进行拉伸,暗部压缩。图像整体变暗,对比度看起来更高。
- γ = 1:线性映射,啥也不干。
你可能会问:“为什么不用线性拉伸?” 因为人眼对亮度的感知本身就是非线性的。伽马校正更符合人眼的视觉特性,处理后的图像看起来更“自然”。
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)
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)是最基础的版本。它的做法是:
- 对图像做高斯模糊,得到光照分量 L 的估计。
- 在 log 域中,用原图减去 L,得到反射分量 R。
- 对 R 做指数变换,还原到正常范围。
公式:
R(x,y) = log(S(x,y)) - log(G(x,y) * S(x,y))
其中 G 是高斯核,* 是卷积操作。
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 的计算依赖于原图各通道的比值。说白了,就是让处理后的图像尽量保持原图的色彩比例。
| 方法 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 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
4.4 本章知识体系
下面这张图总结了本章的核心逻辑,从像素级操作到物理模型,层层递进:
4.5 本章小结
这一章我们聊了三种经典方法:
- 伽马校正:简单粗暴,适合快速提亮暗部,但要注意过曝问题。
- 对数变换:压缩动态范围,适合大光比场景,我常用在预处理阶段。
- Retinex理论:从物理模型出发,SSR/MSR/MSRCR 层层递进,效果越来越好,但计算量和参数复杂度也在增加。