
第 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 再精修。
十三、课后练习
- 基础练习:拍一张室内欠曝照片,分别用 γ=0.5、Log、HE 三种方法提亮,放大暗部区域对比噪声可见度,验证"伽马保形、HE 放噪"的结论。
- 进阶练习 :给
auto_gamma加上二分迭代,让实际输出均值与 target 的误差 < 1 个灰度级,测试 10 张不同亮度的图各需几次迭代。 - 挑战题:实现"自动 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 的窗宽窗位正是它的巅峰应用。