图像处理入门025 | 非线性灰度变换:伽马校正与 Log 变换——暗图救星的两条曲线

第 025 期 | 图像处理算法入门系列 | 2026-09-03

关键词:伽马校正、Log 变换、非线性点运算、自动伽马、LUT 加速、sRGB 显示伽马、暗图增强

承接 024 期 → 025 期 :024 期我们用一条直线(g = α·f + β)搞定亮度与对比度;今晚把直线弯成曲线------伽马与 Log。它们专治一种病:暗部细节挤在一起,线性拉伸救不回来


一、引言:为什么需要"曲线"

024 期结尾留了一个尾巴:面对一张欠曝暗图,线性变换有两个先天缺陷。

text 复制代码
缺陷 1:直线是"一视同仁"的。
  g = 1.4·f + 55 确实能提亮,但它对暗部、中间调、高光的拉伸比例完全相同。

缺陷 2:对比度提升(绕 128 拉伸)会把暗部压死。
  g = 1.9·(f − 128) + 128 会让所有 f < 61 的像素直接截断成 0------
  也就是"黑得一塌糊涂",暗部细节反而全部丢失。

而人眼对暗部变化的敏感度远高于亮部(这正是 sRGB 编码的设计初衷)。我们需要一种变换:在低灰区斜率大(把挤在一起的暗部细节拉开),在高灰区斜率小(亮部反正看得清,不用动)

这就是曲线,也就是今晚的主角:

text 复制代码
伽马校正:  g = 255 · (f / 255)^γ
Log 变换:  g = c · log10(1 + f),其中 c = 255 / log10(256) ≈ 96.34

二、核心原理:幂函数与对数函数的形状魔法

2.1 伽马校正

把像素值归一化到 0, 1,做幂运算,再映射回 0, 255

text 复制代码
g = 255 · (f / 255)^γ
γ 取值 曲线形状 效果 典型场景
γ < 1 低灰区斜率 > 1,曲线上凸 提亮暗部 欠曝修复、暗房照片
γ = 1 对角直线 恒等(不变) ---
γ > 1 低灰区斜率 < 1,曲线下凹 压暗暗部、保护高光 防止过曝、HDR 压缩
text 复制代码
两个锚点是理解的关键(幂运算的数学性质):
  f = 0   →  0^γ = 0      纯黑永远不变
  f = 255 →  1^γ = 1      纯白永远不变
  中间全部被曲线"弯"了:γ < 1 时低灰区被拉伸,γ > 1 时被压缩。

2.2 显示伽马 2.2:为什么这个参数无处不在

历史与生理的双重原因:

text 复制代码
1. CRT 显示器的物理响应近似幂函数 g ∝ f^2.2(电压 → 亮度的非线性)。
2. 人眼对暗部更敏感,sRGB 标准干脆"将错就错":
   图像存储时预先做 γ ≈ 1/2.2 的编码,显示时硬件自动做 γ = 2.2 解码,
   一来一回既补偿了显示器,又把编码精度集中给了人眼最敏感的暗部。

所以今天几乎所有图像都是"伽马压缩过"的。相机 RAW 直出偏暗、游戏引擎里出现 Gamma/Linear 色彩空间切换、显示器校色要调伽马曲线------全是同一个数学。

2.3 Log 变换

对数函数天然"压大保小":

text 复制代码
g = c · log10(1 + f),c = 255 / log10(256) ≈ 96.34
  (加 1 避免 log10(0);除以 log10(256) 保证 f = 255 → g = 255)
变换 参数 暗部提升 亮部影响 可调性
伽马 γ<1 1 个(γ) 强度随 γ 连续可调 同步压暗高光
Log 0 个(天生固定) 固定强提亮 高光几乎不动 弱(可调 c)
text 复制代码
直觉对比:
  Log 变换 ≈ "一键自动挡"------无需调参,暗部立刻可见,适合快速预览、傅里叶频谱显示;
  伽马校正 ≈ "手动挡"------γ 给你连续的控制范围,适合精细修图与自动化 pipeline。

图 1:一张图看懂非线性变换。左图为映射曲线族:γ<1 的曲线在低灰区斜率大于 1(暗部被拉开),Log 曲线最陡;右图为同一渐变条的实际效果,γ=0.25 时暗端被显著拉宽。


三、代码实践

3.1 手写伽马校正(推荐版本)

python 复制代码
import numpy as np

def gamma_correction(img, gamma=1.0):
    """
    伽马校正:g = 255 * (f/255)^gamma
    gamma < 1 提亮暗部,gamma > 1 压暗暗部。
    """
    norm = img.astype(np.float32) / 255.0          # ① 归一化到 [0,1]
    out = np.power(norm, gamma) * 255.0            # ② 幂运算(float32 不会溢出)
    return np.clip(np.round(out), 0, 255).astype(np.uint8)   # ③ 落回 uint8

def log_transform(img, c=None):
    """Log 变换:g = c * log10(1 + f)"""
    if c is None:
        c = 255.0 / np.log10(1 + 255)              # ≈ 96.34,保证 f=255 → 255
    out = c * np.log10(1.0 + img.astype(np.float32))
    return np.clip(np.round(out), 0, 255).astype(np.uint8)
text 复制代码
关键细节:
  1. 归一化到 [0,1] 后幂运算,端点 0^γ=0、1^γ=1 天然在范围内,
     所以伽马校正不像线性变换那样有"溢出"问题------但 round 后仍要 clip 兜底。
  2. 负数不能做浮点幂(会得到 nan),输入必须先归一化到非负区间。
  3. dtype 陷阱:uint8 直接参与 (f/255)^γ 会先整数除法归零------
     必须先 astype(np.float32) 再除。

3.2 LUT 版本(非线性点运算的天然搭档)

伽马是"每个像素做同一个映射",而映射只有 256 种输入------先算好 256 个输出,再查表:

python 复制代码
import cv2
import numpy as np

def gamma_by_lut(img, gamma=1.0):
    lut = np.clip(np.round(
        np.power(np.arange(256, dtype=np.float32) / 255.0, gamma) * 255.0
    ), 0, 255).astype(np.uint8)
    return cv2.LUT(img, lut)
text 复制代码
这笔账很划算:
  NumPy 版:480×640 = 307,200 次浮点幂运算
  LUT 版:  256 次幂运算(一次性) + 307,200 次整数查表
  幂运算比查表贵 1~2 个数量级 → LUT 实测快 10 倍以上(见第五节基准)。

3.3 伽马网格实验

python 复制代码
for g in [0.25, 0.45, 1.0, 2.0, 4.0]:
    out = gamma_correction(dark_img, gamma=g)

图 2:同一张欠曝暗图在 γ = 0.25/0.45/1.0/2.0/4.0 下的输出与直方图。γ<1 时直方图整体右移且被"拉开",暗部细节进入可见区间;γ>1 时分布向暗端压缩堆积。


四、三方案对比:Log vs 伽马 vs HE

暗图提亮的三个候选,各有鲜明的性格(HE 为 022 期直方图均衡化):

text 复制代码
对同一张欠曝图实测(mean / std):
  原图:        暗、对比低
  Log 变换:    暗部提亮明显,直方图平滑变形、形状保留
  伽马(自动):提亮幅度可控,std 提升最自然
  HE:          对比度最强,但直方图被强行摊平------噪声被一起放大(022 期已验证的副作用)
text 复制代码
选型口诀:
  · 快速看一眼暗部在哪儿        → Log(免调参)
  · 修图要精细控制提亮程度      → 伽马(γ 连续可调)
  · 要的是"对比度"而不是"亮度" → HE / CLAHE(022/023 期)
  · 噪声明显的暗图              → 先中值/高斯降噪,再伽马提亮

图 3:Log、伽马(自动 γ)、HE 三方案对比。下排直方图清晰可见:Log/伽马保留直方图形状做平滑变形,HE 则强行摊平导致梳状分布与噪声放大。


五、自动伽马:按均值反解 γ,免调参

手动调 γ 适合修一张图;批量处理 1000 张图时需要一个"自动挡"。方法:先定目标均值,反解 γ

python 复制代码
def auto_gamma(img, target=0.5):
    """
    目标:输出均值 = target(0.5 即中灰 128)
    推导:mean_in^gamma = target  →  gamma = log(target) / log(mean_in)
    """
    mean_in = img.astype(np.float32).mean() / 255.0
    return float(np.log(target) / np.log(mean_in))
text 复制代码
实测(深度欠曝图,mean ≈ 26/255):
  target = 0.40  →  自动 γ = 0.299  →  实际输出 mean = 91
  target = 0.50  →  自动 γ = 0.226  →  实际输出 mean = 114
  target = 0.60  →  自动 γ = 0.167  →  实际输出 mean = 137
text 复制代码
一个必须知道的数学真相(本系列实测验证):
  实际输出均值 ≠ 精确等于 target。
  因为 auto_gamma 的推导假设"均值的伽马 = 伽马的均值",
  而幂运算是非线性函数,均值运算不可交换(Jensen 不等式:
  对凹函数 E[f(X)] ≤ f(E[X]),γ<1 时实际输出均值总是低于 target)。
  工程结论:公式给出的 γ 是极好的初值,精确对齐目标均值
  可再用 2~3 次二分迭代微调(每迭代一次只花几毫秒)。

图 4:自动伽马。目标均值越高 → 自动 γ 越小 → 暗部拉得越猛。注意实际输出均值略低于目标值(Jensen 不等式的效果),但作为免调参初值已经足够好。


六、性能基准:LUT 是非线性变换的最佳拍档

480×640 灰度图,γ = 0.5,三种实现实测:

text 复制代码
Python 双层循环(逐像素查表):  114.6 ms
NumPy 向量化(np.power):        3.69 ms
LUT 查表(cv2.LUT):             0.27 ms

一致性检查:
  LUT  vs NumPy:max|Δ| = 0,完全一致
  loop vs NumPy:max|Δ| ≤ 1(四舍五入路径差异)
实现方式 耗时 说明
Python 循环 ~115 ms 仅教学演示
NumPy 向量化 ~3.7 ms 30 万次浮点幂,通用但浪费
LUT 查表 ~0.27 ms 256 次幂 + 整数查表,快 13 倍
text 复制代码
结论:任何"输入输出都只有 256 种取值"的点运算(伽马/Log/灰度窗/LUT 滤镜),
     都应该用查表实现。这也是 027 期《查找表(LUT)加速》的预告主线。

图 5:三方案性能对比(对数坐标)。LUT 把"逐像素幂运算"降为"逐像素查表",实测比 NumPy 向量化快约 13 倍,且输出逐像素一致。


七、彩色图三方案:BGR 逐通道 / HSV 只动 V / Lab 只动 L

与 024 期同样的老问题:彩色图不能无脑套公式。这次做 γ = 1.6 的压暗实验(与 024 期的增亮形成互补视角):

text 复制代码
方案 A:BGR 三通道逐通道伽马
        ΔH = 1.93°  ΔS = +36.2  ΔV = −22.7
        亮度压暗后暗通道占比相对上升,饱和度反而增加,色相轻微漂移。

方案 B:HSV 只动 V 通道
        ΔH = 0.00°  ΔS = +0.7   ΔV = −22.7
        色相完全不动,饱和度几乎不变------最稳。

方案 C:Lab 只动 L 通道
        ΔH = 1.36°  ΔS = +58.0  ΔV = −39.6
        感知亮度压得最深,饱和度大幅上升(L 被压而色度分量不变),
        视觉上"变浓变暗",有时反而是想要的电影感。
python 复制代码
# 方案 B:HSV 只动 V(推荐)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hsv[:, :, 2] = gamma_by_lut(hsv[:, :, 2], gamma=1.6)
out = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

# 方案 C:Lab 只动 L(感知自然,色彩变浓)
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
lab[:, :, 0] = gamma_by_lut(lab[:, :, 0], gamma=1.6)
out = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
text 复制代码
选型建议(与 024 期结论一脉相承):
  · 不想偏色、只想动亮度       → HSV 只动 V
  · 想要"压暗后颜色更浓"的质感 → Lab 只动 L(电影调色常用)
  · 逐通道 BGR 伽马            → 仅在明确知道要什么时使用

图 6:彩色图伽马三方案对比(γ=1.6 压暗)。B 方案色相零漂移;C 方案饱和度大幅上升带来"变浓"质感;A 方案介于两者之间但色相有轻微漂移。


八、交互式伽马调节器(本地 Trackbar)

python 复制代码
import cv2
import numpy as np

def gamma_by_lut(img, gamma=1.0):
    lut = np.clip(np.round(
        np.power(np.arange(256, dtype=np.float32) / 255.0, gamma) * 255.0
    ), 0, 255).astype(np.uint8)
    return cv2.LUT(img, lut)

img = cv2.imread('your_photo.jpg')
window = 'Gamma Correction (q to quit)'
cv2.namedWindow(window, cv2.WINDOW_NORMAL)
cv2.createTrackbar('gamma x100', window, 100, 400, lambda x: None)  # 0.1 ~ 4.0

while True:
    g = cv2.getTrackbarPos('gamma x100', window) / 100.0
    if g < 0.1:
        g = 0.1
    cv2.imshow(window, gamma_by_lut(img, g))
    if cv2.waitKey(30) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

⚠️ 这段代码需要本地有图形界面;由于 LUT 预计算只需 256 次幂运算,拖动滑杆也是实时帧率。

图 7:参数网格预览。三行分别是:纯伽马、"伽马+线性压对比"组合、Log→伽马级联。级联使用是修图软件"曲线工具"的雏形。


九、Pipeline 定位:线性 vs 伽马,暗图第一步选谁

把两条路线接到同一条增强链上正面交锋(后端都是 023 期的 CLAHE):

text 复制代码
路线① 线性(024 期滑杆组合):g = 1.4·f + 55
        → 提亮后 mean = 78,std = 16 ------ 亮度上来了,但对比度平庸

路线② 伽马(本期自动 γ ≈ 0.3):g = 255·(f/255)^γ
        → 提亮后 mean = 115,std = 40 ------ 亮度对比度双优

两条路线再接 CLAHE:
  线性 + CLAHE:mean = 85,std = 22
  伽马 + CLAHE:mean = 116,std = 41
text 复制代码
结论(028 期自动照片增强器的选型依据):
  暗图的"第一步粗调"应该优先考虑伽马校正:
  线性变换提亮 = 整体平移,暗部细节的相对层次不变;
  伽马提亮 = 非线性拉伸,暗部层次被真正"展开",
  后续 CLAHE 的局部增强才有料可挖。
  线性变换的用武之地是"亮度已经正常、只差一点对比度"的图。

图 8:暗图修复两条路线。上线性路线提亮后整体灰蒙(std=16),下伽马路线暗部层次被真正展开(std=40),接 CLAHE 后差距进一步拉大。


十、可视化实验总览

本次共 8 个实验,对应 8 张可视化图:

# 实验 图文件 验证目标
1 γ 曲线族与渐变条 025_curves_principle.png 一张图看懂曲线形状
2 伽马网格 025_gamma_grid.png γ<1 提亮 / γ>1 压暗
3 Log vs 伽马 vs HE 025_log_vs_gamma.png 三方案性格对比
4 自动伽马 025_auto_gamma.png 免调参 + Jensen 偏差
5 性能与一致性 025_lut_benchmark.png LUT 快 13 倍
6 彩色图三方案 025_color_gamma.png BGR/HSV/Lab 谁最稳
7 参数网格 025_slider_grid.png 等效滑杆 + 级联组合
8 Pipeline 定位 025_pipeline_position.png 线性 vs 伽马选型

代码运行后,上述 PNG 全部自动生成到 code/output/ 目录。


十一、产品推荐

层级 移动端 App 专业级工具 开源/免费 适用场景
伽马/曲线调整 Snapseed(曲线)、Lightroom Mobile(色调曲线)、VSCO Adobe Photoshop(曝光/伽马/曲线)、Capture One、DaVinci Resolve(Gamma 色彩空间) GIMP(曲线)、Darktable、RawTherapee 修图调色
显示校色 手机系统显示色彩模式(P3/sRGB 切换) DisplayCAL + 校色仪、X-Rite i1Display、Calibrite DisplayCAL(开源) 显示器伽马校准
科学可视化 --- ImageJ/Fiji、MATLAB(imadjust) ImageJ(免费) 频谱/医学图显示
影视调色 --- DaVinci Resolve(Gamma 标签页)、Baselight --- Log 素材还原
text 复制代码
选择建议:
  · 手机快速提亮暗图:Snapseed "曲线"工具拉一根 γ<1 的上凸曲线,就是今晚的数学。
  · 显示器校色:DisplayCAL 免费专业,把显示器伽马校到 sRGB(≈2.2)标准。
  · 影视行业:DaVinci Resolve 的 Gamma 色彩空间就是字面意义的本课主角。

十二、常见问题 FAQ

Q1:γ = 2.2 到底是提亮还是压暗?

A:看方向。对图像做 g = (f)^2.2 是压暗(编码方向);显示器硬件做 γ=2.2 解码是还原。日常说"显示器伽马 2.2"指的是解码特性,而修图软件里的"伽马滑杆 < 1"是提亮编码。抓住"γ 作用于谁"就不会混。

Q2:伽马校正会有 uint8 溢出问题吗?

A:归一化后幂运算端点天然在 0,1 内,理论不溢出。但浮点精度和 round 仍建议保留 clip 兜底。真正的陷阱是 dtype:uint8 先除 255 会整数归零,必须先转 float32。

Q3:自动伽马的输出均值为什么达不到 target?

A:推导假设"均值先取再幂"等于"先幂再取均值",但幂函数非线性,两者不等(Jensen 不等式:γ<1 时实际均值偏低)。工程上把公式结果当初值,二分迭代 2~3 次即可精确对齐。

Q4:Log 变换和伽马 < 1 效果好像,怎么选?

A:Log 免调参、高光几乎不动,适合"看一眼"和傅里叶频谱显示;伽马可控性强,适合批量 pipeline 和精细修图。需要严格保持端点时用伽马(0→0、255→255),Log 也满足(log10(1)=0),但中间行为不同。

Q5:为什么彩色图做 Lab 伽马后颜色"变浓"了?

A:伽马只压 L(明度)通道,a/b(色度)分量数值不变。明度降了、色度占比相对升高,视觉饱和度就上升(实测 ΔS=+58)。这既是陷阱也是特性------电影调色里的"压暗提饱和"就是这个原理。

Q6:伽马和 022/023 期的 HE/CLAHE 有什么本质区别?

A:伽马/Log 是固定数学形状 的点运算(与图像内容无关);HE/CLAHE 是数据驱动的(变换曲线由直方图算出)。前者可控可复现,后者自适应但可能放大噪声。增强链里常常是伽马先粗调、CLAHE 再精修。


十三、课后练习

  1. 基础练习:拍一张室内欠曝照片,分别用 γ=0.5、Log、HE 三种方法提亮,放大暗部区域对比噪声可见度,验证"伽马保形、HE 放噪"的结论。
  2. 进阶练习 :给 auto_gamma 加上二分迭代,让实际输出均值与 target 的误差 < 1 个灰度级,测试 10 张不同亮度的图各需几次迭代。
  3. 挑战题:实现"自动 Log 变换":根据图像均值自适应计算常数 c(提示:让输出均值落在 100~150 区间),并与自动伽马在 5 类图像上对比鲁棒性。

十四、完整可运行脚本

bash 复制代码
# 环境要求
Python 3.8+
OpenCV 4.5+ / 5.x
NumPy 1.20+
Matplotlib 3.4+

# 运行
python code/025_gamma_log_transform.py

运行后 code/output/ 下会生成 8 张 PNG,对应本节实验。


下期预告(第 026 期):分段线性变换------灰度窗与阶梯变换。把直线折成几段,实现"只拉伸中间调、冻结两端"的精细控制,医学影像 CT 的窗宽窗位正是它的巅峰应用。

相关推荐
AndrewHZ3 小时前
图像处理入门024 | 线性灰度变换:亮度与对比度调整
图像处理·性能基准·线性灰度变换·亮度 β·对比度 α·uint8溢出·hsv/lab
岁月蹉跎的一杯酒10 小时前
格科微内置 ISP 图像效果调试(格科微 YUV Sensor)—GC2145
图像处理·isp·gc2145
FPGA开源工坊13 小时前
ISP图像处理--Bayer Raw格式
图像处理·人工智能·verilog·fpga·isp
江畔柳前堤1 天前
具身智能全景深度指南(2026年9月版):从“会聊天的AI“到“能干活的机器“
大数据·javascript·图像处理·人工智能·分布式·智慧城市·原型模式
安河桥。1 天前
Android视频流处理模块硬件加速器数据流转技术说明
android·图像处理·isp
牧羊人.3332 天前
计算机视觉基础 第13章 |背景建模与运动目标检测
图像处理·人工智能·目标检测·计算机视觉·目标跟踪
牧羊人.3332 天前
计算机视觉基础第15章|DNN实现图像风格迁移
图像处理·人工智能·深度学习·opencv·计算机视觉
拿我格子衫来2 天前
LaserWeb4:矢量图与位图如何变成 G-code
图像处理·图形渲染
Dfreedom.2 天前
目标检测后处理核心:NMS非极大值抑制详解
图像处理·人工智能·深度学习·目标检测·目标跟踪