4. 高级技巧:伽马校正、对数变换、CLAHE与多尺度Retinex
前面我们聊了线性拉伸和直方图均衡,这些方法对付大部分场景已经够用了。但总有些「硬骨头」——比如背光拍摄的人脸、雾霾天的监控画面、或者医学X光片那种整体偏暗但局部细节又很重要的图像。这时候,就该上点高级技巧了。
我个人习惯把这四种方法称为「四大金刚」:伽马校正、对数变换、CLAHE、多尺度Retinex。它们各自擅长处理不同类型的非线性光照问题。今天咱们一个一个拆开看。
4.1 伽马校正:调整感知亮度
伽马校正最早来自CRT显示器的物理特性——输入电压和显示亮度不是线性关系。后来发现,人眼对暗部变化的敏感度远高于亮部,所以伽马校正反而成了一种「感知均匀」的编码方式。
公式很简单:O = I ^ (1/γ)。当 γ > 1 时,暗部被拉伸,亮部被压缩;γ < 1 时则相反。
import cv2
import numpy as np
def gamma_correction(img, gamma=1.0):
# 归一化到 [0, 1]
normalized = img / 255.0
# 伽马变换
corrected = np.power(normalized, 1/gamma)
# 还原到 [0, 255]
return np.uint8(corrected * 255)
# 使用示例
img = cv2.imread('dark_face.jpg', cv2.IMREAD_GRAYSCALE)
result = gamma_correction(img, gamma=2.2)
4.2 对数变换:压缩动态范围
对数变换的核心思想是:把指数增长映射为线性增长。说白了,就是让暗部细节「浮」出来,同时不让亮部过曝。
公式:O = c * log(1 + I)。c 是缩放常数,通常取 255 / log(256)。
def log_transform(img, c=None):
if c is None:
c = 255 / np.log(1 + 255)
# 防止 log(0)
img_float = np.float32(img) + 1
transformed = c * np.log(img_float)
return np.uint8(np.clip(transformed, 0, 255))
对数变换和伽马校正有什么区别?我画了张图帮你理解:
4.3 CLAHE:限制对比度的自适应直方图均衡
普通直方图均衡有个毛病:它把整张图当成一个整体处理。如果图像一部分很暗、一部分很亮,均衡后暗部可能过曝,亮部可能丢失细节。
CLAHE(Contrast Limited Adaptive Histogram Equalization)的解决思路是:把图像切成小块,每个小块单独做直方图均衡,然后用插值消除块之间的边界。最关键的是——它限制了对比度的放大倍数,防止噪声被过度增强。
| 参数 | 含义 | 典型值 | 我的建议 |
|---|---|---|---|
| clipLimit | 对比度限制阈值 | 2.0 ~ 5.0 | 先试 3.0,效果不够再往上加 |
| tileGridSize | 分块大小 | (8,8) ~ (16,16) | 小图用 8x8,大图用 16x16 |
def clahe_enhance(img, clip_limit=3.0, grid_size=(8,8)):
# 创建 CLAHE 对象
clahe = cv2.createCLAHE(
clipLimit=clip_limit,
tileGridSize=grid_size
)
# 应用 CLAHE
enhanced = clahe.apply(img)
return enhanced
# 彩色图像处理:先转 LAB,只处理 L 通道
img_color = cv2.imread('low_contrast.jpg')
lab = cv2.cvtColor(img_color, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
l_enhanced = clahe_enhance(l, clip_limit=2.5)
lab_enhanced = cv2.merge([l_enhanced, a, b])
result = cv2.cvtColor(lab_enhanced, cv2.COLOR_LAB2BGR)
4.4 多尺度 Retinex:分离光照与反射
Retinex 理论认为:人眼看到的图像 = 光照分量 × 反射分量。光照是低频的、缓慢变化的;反射是高频的、包含细节的。我们想要的增强,本质上是「去掉不均匀光照,保留或增强反射细节」。
多尺度 Retinex(MSR)的做法是:用多个不同尺度的高斯滤波估计光照,然后从原图中「除掉」光照,剩下的就是反射。多个尺度加权平均,兼顾局部和全局。
def multi_scale_retinex(img, scales=[15, 80, 250], weights=None):
if weights is None:
weights = [1/3] * len(scales)
img_float = np.float32(img) + 1.0
img_log = np.log(img_float)
msr = np.zeros_like(img_float)
for scale, weight in zip(scales, weights):
# 高斯模糊估计光照
blur = cv2.GaussianBlur(img_float, (0,0), scale)
blur_log = np.log(blur)
# 单尺度 Retinex
ssr = img_log - blur_log
msr += weight * ssr
# 归一化到 [0, 255]
msr = cv2.normalize(msr, None, 0, 255, cv2.NORM_MINMAX)
return np.uint8(msr)
这四种方法各有侧重,我整理了一张对比表:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 伽马校正 | 整体偏暗/偏亮 | 简单、可逆、感知均匀 | 无法处理局部光照不均 |
| 对数变换 | 动态范围极大(如HDR) | 强暗部提升、压缩亮部 | 噪声放大明显 |
| CLAHE | 局部对比度不足 | 自适应、限制噪声 | 参数敏感、彩色需特殊处理 |
| 多尺度Retinex | 光照不均、雾霾、阴影 | 光照分离、细节保留好 | 计算量大、可能有光晕 |
最后说一句:这些方法不是互斥的。我经常把 CLAHE 和伽马校正串起来用——先用 CLAHE 提升局部对比度,再用伽马微调整体亮度。效果往往 1+1 > 2。但要注意顺序:先局部后整体,先细节后亮度。