为什么要进行Gamma校正
图像的 gamma 校正是一种图像处理技术,用于调整图像的亮度和对比度,让显示设备显示的亮度和对比度更符合人眼的感知。Gamma 校正主要用于修正显示设备的非线性响应,以及在图像处理中进行色彩校正和图像增强。
以前,大多数监视器是阴极射线管显示器(CRT)。这些显示器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做显示器Gamma。
Gamma也叫灰度系数,每种显示设备都有自己的Gamma值,都不相同,有一个公式:设备输出亮度 = 电压的Gamma次幂,任何设备Gamma基本上都不会等于1,等于1是一种理想的线性状态,这种理想状态是:如果电压和亮度都是在0到1的区间,那么多少电压就等于多少亮度。对于CRT,Gamma通常为2.2,因而,输出亮度 = 输入电压的2.2次幂,你可以从本节第二张图中看到Gamma2.2实际显示出来的总会比预期暗,相反Gamma0.45就会比理想预期亮,如果你讲Gamma0.45叠加到Gamma2.2的显示设备上,便会对偏暗的显示效果做到校正,这个简单的思路就是本节的核心
人类所感知的亮度恰好和CRT所显示出来相似的指数关系非常匹配(我猜并不是巧合,可能是物理原因)。为了更好的理解所有含义,请看下面的图片:
第一行是人眼所感知到的正常的灰阶,亮度要增加一倍(比如从0.1到0.2)你才会感觉比原来变亮了一倍(这里的意思是说比如一个东西的亮度0.3,让人感觉它比原来变亮一倍,那么现在这个亮度应该成为0.6,而不是0.4,也就是说人眼感知到的亮度的变化并非线性均匀分布的。问题的关键在于这样的一倍相当于一个亮度级,例如假设0.1、0.2、0.4、0.8是我们定义的四个亮度级别,在0.1和0.2之间人眼只能识别出0.15这个中间级,而虽然0.4到0.8之间的差距更大,这个区间人眼也只能识别出一个颜色)。然而,当我们谈论光的物理亮度,比如光源发射光子的数量的时候,底部(第二行)的灰阶显示出的才是物理世界真实的亮度。如底部的灰阶显示,亮度加倍时返回的也是真实的物理亮度(译注:这里亮度是指光子数量和正相关的亮度,即物理亮度,前面讨论的是人的感知亮度;物理亮度和感知亮度的区别在于,物理亮度基于光子数量,感知亮度基于人的感觉,比如第二个灰阶里亮度0.1的光子数量是0.2的二分之一),但是由于这与我们的眼睛感知亮度不完全一致(对比较暗的颜色变化更敏感),所以它看起来有差异。
因为人眼看到颜色的亮度更倾向于顶部的灰阶,显示器使用的也是一种指数关系(电压的2.2次幂),所以物理亮度通过监视器能够被映射到顶部的非线性亮度;因此看起来效果不错(译注:CRT亮度是是电压的2.2次幂而人眼相当于2次幂,因此CRT这个缺陷正好能满足人的需要)。
显示器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于显示器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:
点线代表线性颜色/亮度值(译注:这表示的是理想状态,Gamma为1),实线代表显示器显示的颜色。如果我们把一个点线线性的颜色翻一倍,结果就是这个值的两倍。比如,光的颜色向量L¯=(0.5,0.0,0.0)代表的是暗红色。如果我们在线性空间中把它翻倍,就会变成(1.0,0.0,0.0),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的显示器上,显示器上显示的实际颜色就会是(0.218,0.0,0.0)(0.218,0.0,0.0)。在这儿问题就出现了:当我们将理想中直线上的那个暗红色翻一倍时,在显示器上实际上亮度翻了4.5倍以上!
直到现在,我们还一直假设我们所有的工作都是在线性空间中进行的(Gamma为1),但最终还是要把所哟的颜色输出到显示器上,所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的,在我们的显示器上很少能够正确地显示。出于这个原因,我们(以及艺术家)通常将光照值设置得比本来更亮一些(由于显示器会将其亮度显示的更暗一些),如果不是这样,在线性空间里计算出来的光照就会不正确。同时,还要记住,监视器所显示出来的图像和线性图像的最小亮度是相同的,它们最大的亮度也是相同的;只是中间亮度部分会被压暗。
因为所有中间亮度都是线性空间计算出来的(计算的时候假设Gamma为1),显示器显以后,实际上都会不正确。当使用更高级的光照算法时,这个问题会变得越来越明显,你可以看看下图
Gamma校正
Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用显示器Gamma的倒数。回头看前面的Gamma曲线图,会有一个短划线,它是显示器Gamma曲线的翻转曲线。我们在颜色显示到显示器的时候把每个颜色输出都加上这个翻转的Gamma曲线,这样应用了显示器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮,所以虽然监视器使它们变暗,但是我们又将其平衡回来了。
我们来看另一个例子。还是那个暗红色(0.5,0.0,0.0)。在将颜色显示到显示器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在显示器上相当于降低了2.22.2次幂的亮度,所以倒数就是1/2.2次幂。Gamma校正后的暗红色就会成为
(0.5,0.0,0.0)^1/2.2=(0.5,0.0,0.0)^0.45=(0.73,0.0,0.0)(0.5,0.0,0.0)^1/2.2=(0.5,0.0,0.0)^0.45=(0.73,0.0,0.0)。
校正后的颜色接着被发送给监视器,最终显示出来的颜色是
(0.73,0.0,0.0)^2.2=(0.5,0.0,0.0)。
你会发现使用了Gamma校正,监视器最终会显示出我们在应用中设置的那种线性的颜色。
2.2通常是是大多数显示设备的大概平均gamma值。基于gamma2.2的颜色空间叫做sRGB颜色空间。每个监视器的gamma曲线都有所不同,但是gamma2.2在大多数监视器上表现都不错。出于这个原因,游戏经常都会为玩家提供改变游戏gamma设置的选项,以适应每个监视器(译注:现在Gamma2.2相当于一个标准,后文中你会看到。但现在你可能会问,前面不是说Gamma2.2看起来不是正好适合人眼么,为何还需要校正。这是因为你在程序中设置的颜色,比如光照都是基于线性Gamma,即Gamma1,所以你理想中的亮度和实际表达出的不一样,如果要表达出你理想中的亮度就要对这个光照进行校正)。
在 gamma 校正中,通过应用一个指数函数来调整图像的像素值,这个指数函数通常被称为 gamma 曲线。Gamma 曲线的形状决定了图像的亮度和对比度的变化程度。
一般而言,gamma 校正可以分为两种情况:
-
增加 gamma 值(大于1):这会使得图像中较暗的部分变得更暗,而较亮的部分变得更亮。这样做可以增强图像的对比度,使得细节更加清晰。这种调整通常称为对比度增强。
-
减小 gamma 值(小于1):这会使得图像中较亮的部分变得更暗,而较暗的部分变得更亮。这样做可以降低图像的对比度,使得图像更柔和。这种调整通常称为图像的色彩校正。
Gamma校正的算法
Gamma 校正是通过应用一个指数函数来调整图像的像素值,即将图像中的每个像素值 映射到一个新值
这里的 I 是原始的像素值, γ 是所谓的 gamma 参数,决定了调整的程度。
伽马变换对图像的修正作用其实就是通过增强低灰度或高灰度的细节实现的, 从下面的伽马曲线可以直观理解:
(生成这个交互式曲线图的python代码在文章的后面有提供)
Gamma 校正的算法步骤如下:
-
确定 gamma 值:首先需要确定用于 gamma 校正的 gamma 参数值。通常,gamma 参数取值在 0 到 1 之间会使图像变亮,而取值大于 1 会使图像变暗。
-
应用指数变换:对于图像中的每个像素,将原始像素值 应用指数变换得到校正后的像素值 。
-
归一化处理:根据需要,对校正后的像素值进行归一化处理,使得调整后的像素值范围在合适的范围内,通常是 0 到 255。
-
输出校正后的图像:将处理后的像素值重新组合成一个图像,并输出校正后的图像。
python实现图像的Gamma校正
PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换-Anlogic-安路论坛-FPGA CPLD-ChipDebug
这段代码使用 OpenCV 读取和处理图像,并使用 Matplotlib 显示图像。它实现了对图像的两种不同的 Gamma 变换(sqrt 和 square),并显示了原始图像以及两种 Gamma 变换后的图像对比。
Matlab实现图像的Gamma校正
PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换-Anlogic-安路论坛-FPGA CPLD-ChipDebug
这段 MATLAB 代码实现了对图像进行 Gamma 变换的功能,并提供了两种不同类型的 Gamma 变换:sqrt
和 square
。
gamma_correction
函数接受三个参数:image
:输入的彩色图像。gamma_type
:指定 Gamma 变换的类型,支持 ‘sqrt’ 和 ‘square’ 两种。c
:常数系数,用于调节变换后的亮度范围。
该函数首先将输入图像分割成三个通道(蓝色、绿色和红色)。然后根据指定的 Gamma 类型进行 Gamma 变换,计算每个通道的 Gamma 变换后的像素值。最后,将各通道的像素值进行缩放以控制亮度范围,并合并三个通道,返回进行 Gamma 变换后的图像。
img_gamma
函数负责读取图像并调用gamma_correction
函数进行 Gamma 变换。它显示原始图像和两种 Gamma 变换后的彩色图像对比,以直观展示 Gamma 变换的效果。
这段代码的主要功能是通过 Gamma 变换来调整图像的亮度和对比度,从而改善图像的视觉效果。
工程解析
工程层次图
与demo18相比,只是多了一个img_gamma的模块,也就是下面这一段代码,在从SDRAM读出来之后,经它处理后再输出hdmi_tx模块。
代码解析
对于FPGA做log运算最好的办法是我们先用matlab或者python把gamma运算的结果计算出来,然后存到FPGA的ROM中,运行时FPGA从ROM中查表获取c*f(r)运算的结果即可。
这个工程的算法流程就是一个查表操作,img_gamma.v代码中的注释也很详细,请不一一解释了。
代码中我们提供了GAMMA_SQUARE和GAMMA_SQRT两个gamm计算方法,它们分别表示什么含义呢?
GAMMA_SQUARE
和 GAMMA_SQRT
是两种不同的 Gamma 校正算法的表示。
GAMMA_SQUARE
表示使用 Gamma 平方校正算法。在这种算法中,对输入信号的每个分量进行平方操作,以实现 Gamma 校正。GAMMA_SQRT
表示使用 Gamma 平方根校正算法。在这种算法中,对输入信号的每个分量进行平方根操作,以实现 Gamma 校正。
这两种算法的选择会影响最终的图像效果,因为它们在处理输入信号时采用了不同的数学操作。
这两种算法的rom.v如何生成呢?我们提供了matlab代码和python代码。
python版gamma rom生成代码
PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换-Anlogic-安路论坛-FPGA CPLD-ChipDebug
matlab版代码
PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换-Anlogic-安路论坛-FPGA CPLD-ChipDebug
这段 MATLAB 代码完成了以下功能:
- 定义了灰度级数目和灰度级数组。
- 计算了使用平方根算法和平方算法的伽马变换后的灰度值。
- 将伽马变换后的灰度值写入了 COE 文件中,用于 Verilog 仿真。
- 生成了伽马变换的 Verilog 源文件,其中包含模块定义、端口声明和组合逻辑。
- 绘制了原始灰度级和两种伽马变换方式的输出灰度级的图像。
管脚约束
与PotatoPie 4.0 实验教程(18) —— FPGA实现OV5640摄像头采集以SDRAM作为显存进行HDMI输出显示相同,不作赘述。
时序约束
与PotatoPie 4.0 实验教程(18) —— FPGA实现OV5640摄像头采集以SDRAM作为显存进行HDMI输出显示相同,不作赘述。
实验结果
由于rgb用的同一个gamma,导致画面偏紫,大家可以自行调整参数生成不同的gamma表。