PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换-Anlogic-安路论坛-FPGA CPLD-ChipDebug

PotatoPie 4.0 实验教程(23) —— FPGA实现摄像头图像伽马(Gamma)变换

手机扫码

20240416075513933-1713225291635

链接直达

https://item.taobao.com/item.htm?ft=t&id=776516984361

为什么要进行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所显示出来相似的指数关系非常匹配(我猜并不是巧合,可能是物理原因)。为了更好的理解所有含义,请看下面的图片:

20240413123542135-image

第一行是人眼所感知到的正常的灰阶,亮度要增加一倍(比如从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这个缺陷正好能满足人的需要)。

显示器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于显示器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:

20240413123600570-image

点线代表线性颜色/亮度值(译注:这表示的是理想状态,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),显示器显以后,实际上都会不正确。当使用更高级的光照算法时,这个问题会变得越来越明显,你可以看看下图

20240413123906198-image

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 校正可以分为两种情况:

  1. 增加 gamma 值(大于1):这会使得图像中较暗的部分变得更暗,而较亮的部分变得更亮。这样做可以增强图像的对比度,使得细节更加清晰。这种调整通常称为对比度增强。

  2. 减小 gamma 值(小于1):这会使得图像中较亮的部分变得更暗,而较暗的部分变得更亮。这样做可以降低图像的对比度,使得图像更柔和。这种调整通常称为图像的色彩校正。

Gamma校正的算法

Gamma 校正是通过应用一个指数函数来调整图像的像素值,即将图像中的每个像素值 映射到一个新值

20240406205715333-QianJianTec1712408136185

是所谓的 gamma 参数,决定了调整的程度。

伽马变换对图像的修正作用其实就是通过增强低灰度或高灰度的细节实现的, 从下面的伽马曲线可以直观理解:

20240413175951635-image

(生成这个交互式曲线图的python代码在文章的后面有提供)

Gamma 校正的算法步骤如下:

  1. 确定 gamma 值:首先需要确定用于 gamma 校正的 gamma 参数值。通常,gamma 参数取值在 0 到 1 之间会使图像变亮,而取值大于 1 会使图像变暗。

  2. 应用指数变换:对于图像中的每个像素,将原始像素值 应用指数变换得到校正后的像素值 。

  3. 归一化处理:根据需要,对校正后的像素值进行归一化处理,使得调整后的像素值范围在合适的范围内,通常是 0 到 255。

  4. 输出校正后的图像:将处理后的像素值重新组合成一个图像,并输出校正后的图像。

python实现图像的Gamma校正

这段代码使用 OpenCV 读取和处理图像,并使用 Matplotlib 显示图像。它实现了对图像的两种不同的 Gamma 变换(sqrt 和 square),并显示了原始图像以及两种 Gamma 变换后的图像对比。

以下是代码的详细说明:

  1. 导入必要的库:

    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    import os
    • cv2:用于图像处理。
    • numpy:用于数组操作。
    • matplotlib.pyplot:用于绘制图像。
    • os:用于操作文件路径。
  2. 定义 Gamma 变换函数:

    def gamma_correction(image, gamma_type='sqrt', c=1):
    ...

    该函数对输入的彩色图像进行 Gamma 变换。参数包括图像、Gamma 变换类型和常数系数。函数返回进行 Gamma 变换后的图像。

  3. 获取当前文件的路径:

    current_file_path = __file__
    current_file_dir = os.path.dirname(current_file_path)

    获取当前脚本文件的路径,并使用 os.path.dirname() 函数提取目录部分,存储在 current_file_dir 变量中。

  4. 读取图像:

    image = cv2.imread(current_file_dir+'/Lena.jpg')

    使用 cv2.imread() 函数读取名为 ‘Lena.jpg’ 的图像文件,并将其存储在名为 image 的变量中。

  5. 进行两种 Gamma 变换:

    gamma_sqrt = gamma_correction(image, gamma_type='sqrt', c=1)
    gamma_square = gamma_correction(image, gamma_type='square', c=1)

    分别调用 gamma_correction() 函数对原始图像进行两种不同类型的 Gamma 变换,分别为平方根型和平方型,并将结果存储在 gamma_sqrtgamma_square 变量中。

  6. 显示原始图像和两种 Gamma 变换后的图像对比:

    plt.figure(figsize=(12, 4))
    
    # 显示原始图像
    plt.subplot(1, 3, 1)
    plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    plt.title('Original Image')
    plt.axis('off')
    
    # 显示 Gamma (sqrt) 变换后的图像
    plt.subplot(1, 3, 2)
    plt.imshow(cv2.cvtColor(gamma_sqrt, cv2.COLOR_BGR2RGB))
    plt.title('Gamma (sqrt) Corrected Image')
    plt.axis('off')
    
    # 显示 Gamma (square) 变换后的图像
    plt.subplot(1, 3, 3)
    plt.imshow(cv2.cvtColor(gamma_square, cv2.COLOR_BGR2RGB))
    plt.title('Gamma (square) Corrected Image')
    plt.axis('off')
    
    plt.show()

    使用 plt.figure() 创建一个图像窗口,然后使用 plt.subplot() 在窗口中创建三个子图,分别显示原始图像、Gamma (sqrt) 变换后的图像和 Gamma (square) 变换后的图像。最后使用 plt.show() 显示图像。

20240414183433663-image

Matlab实现图像的Gamma校正

这段代码实现了图像的 Gamma 变换,并提供了两种不同的 Gamma 变换类型:平方根变换和平方变换。以下是代码的详细说明:

  1. 主函数 img_gamma():

    • 获取当前文件路径,并使用 fileparts() 函数提取目录部分。
    • 读取名为 ‘Lena.jpg’ 的图像文件。
    • 调用 gamma_correction() 函数两次,分别进行平方根变换和平方变换,得到两种不同的 Gamma 变换结果。
    • 使用 subplot() 函数在一个图像窗口中显示原始图像和两种 Gamma 变换后的图像,并设置标题。
  2. Gamma 变换函数 gamma_correction():

    • 对图像进行 Gamma 变换,其中参数包括输入图像 image、Gamma 变换类型 gamma_type 和常数系数 c
    • 将输入图像的三个通道(蓝色、绿色和红色)分别提取出来。
    • 根据指定的 Gamma 变换类型,对每个通道进行不同的 Gamma 变换:
      • 如果 gamma_type 为 ‘sqrt’,则进行平方根变换;
      • 如果 gamma_type 为 ‘square’,则进行平方变换。
    • 对变换后的像素值进行缩放以控制亮度范围,并将其转换为 uint8 类型。
    • 将三个通道合并成一个彩色图像,并返回结果。

这段代码的主要功能是通过 Gamma 变换来调整图像的亮度和对比度,从而改善图像的视觉效果。

20240415124047923-1713156027775

工程解析

工程层次图

20240413133430877-image

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_SQUAREGAMMA_SQRT 是两种不同的 Gamma 校正算法的表示。

  • GAMMA_SQUARE 表示使用 Gamma 平方校正算法。在这种算法中,对输入信号的每个分量进行平方操作,以实现 Gamma 校正。
  • GAMMA_SQRT 表示使用 Gamma 平方根校正算法。在这种算法中,对输入信号的每个分量进行平方根操作,以实现 Gamma 校正。

这两种算法的选择会影响最终的图像效果,因为它们在处理输入信号时采用了不同的数学操作。

这两种算法的rom.v如何生成呢?我们提供了matlab代码和python代码。

python版gamma rom生成代码

这段代码是用于进行伽马变换的图像处理。在这段代码中,伽马变换分别采用了平方根算法和平方算法。

下面是对代码的详细说明:

  1. 导入模块

    • 使用了NumPy库进行数组操作。
    • 使用了Matplotlib库进行图像绘制。
  2. 定义变量

    • num:灰度级数目,这里设置为256,表示图像的灰度级范围为0到255。
    • r:表示灰度级的数组,从0到255。
    • c:灰度缩放系数,用于控制变换的幅度。
  3. 计算伽马变换后的灰度值

    • g_sqrt:使用平方根算法计算的伽马变换后的灰度值。
    • g_square:使用平方算法计算的伽马变换后的灰度值。
  4. 将伽马变换后的灰度值写入COE文件中

    • 生成两个COE文件,分别存储平方根算法和平方算法计算得到的伽马变换后的灰度值。
  5. 生成伽马变换的Verilog源文件

    • 生成了两个Verilog源文件,分别对应平方根算法和平方算法计算得到的伽马变换。
    • Verilog文件中包含了模块定义、端口声明、数据存储和组合逻辑。
  6. 绘制图像

    • 使用Matplotlib库绘制了原始灰度级和两种伽马变换后的灰度级的曲线图,以展示伽马变换的效果。
    • x轴表示输入的原始灰度级,y轴表示输出的伽马变换后的灰度级。
    • 分别绘制了原始灰度级、平方根算法和平方算法得到的伽马变换曲线,并添加了图例和标题。

这段代码实现了对图像进行伽马变换,并将变换后的灰度值保存到COE文件和Verilog源文件中,同时通过图表展示了伽马变换的效果。

matlab版代码

这段 MATLAB 代码完成了以下功能:

  1. 定义了灰度级数目和灰度级数组。
  2. 计算了使用平方根算法和平方算法的伽马变换后的灰度值。
  3. 将伽马变换后的灰度值写入了 COE 文件中,用于 Verilog 仿真。
  4. 生成了伽马变换的 Verilog 源文件,其中包含模块定义、端口声明和组合逻辑。
  5. 绘制了原始灰度级和两种伽马变换方式的输出灰度级的图像。

以下是代码的详细说明:

  1. 定义灰度级数目和灰度级数组:

    num = 256;
    r = 0:255;

    定义了灰度级数目为 256,并创建了一个从 0 到 255 的灰度级数组 r

  2. 灰度缩放系数:

    c = 16;

    定义了一个灰度缩放系数 c,用于控制伽马变换的幅度。

  3. 计算伽马变换后的灰度值(平方根算法):

    g_sqrt = c * sqrt(r);

    使用平方根算法计算伽马变换后的灰度值,并将结果存储在名为 g_sqrt 的变量中。

  4. 将伽马变换后的灰度值写入 COE 文件中:

    coe_file_name_sqrt = 'gamma_sqrt_para_256.coe';
    fid = fopen(coe_file_name_sqrt, 'w');

    创建了一个名为 gamma_sqrt_para_256.coe 的 COE 文件,并打开用于写入数据的文件句柄。

  5. 生成伽马变换的 Verilog 源文件:

    verilog_file_name_sqrt = 'img_rom_gamma_sqrt_para.v';
    fid = fopen(verilog_file_name_sqrt, 'w');

    创建了一个名为 img_rom_gamma_sqrt_para.v 的 Verilog 源文件,并打开用于写入数据的文件句柄。

  6. 计算伽马变换后的灰度值(平方算法):

    g_square = (r .^ 2) / 255;

    使用平方算法计算伽马变换后的灰度值,并将结果存储在名为 g_square 的变量中。

  7. 将伽马变换后的灰度值写入 COE 文件中:

    coe_file_name_square = 'gamma_square_para_256.coe';
    fid = fopen(coe_file_name_square, 'w');

    创建了一个名为 gamma_square_para_256.coe 的 COE 文件,并打开用于写入数据的文件句柄。

  8. 生成伽马变换的 Verilog 源文件:

    verilog_file_name_square = 'img_rom_gamma_square_para.v';
    fid = fopen(verilog_file_name_square, 'w');

    创建了一个名为 img_rom_gamma_square_para.v 的 Verilog 源文件,并打开用于写入数据的文件句柄。

  9. 绘制图像:

    figure;
    plot(r, 'DisplayName', 'Original');
    hold on;
    plot(g_sqrt, 'DisplayName', 'Gamma (Square Root)');
    plot(g_square, 'DisplayName', 'Gamma (Square)');
    hold off;
    xlabel('Input Gray Level');
    ylabel('Output Gray Level');
    title('Gamma Transformation');
    legend;

    使用 plot() 函数绘制原始灰度级和两种不同伽马变换算法的输出灰度级曲线,并设置图像的标题、坐标轴标签和图例。

20240415214728428-image

管脚约束

PotatoPie 4.0 实验教程(18) —— FPGA实现OV5640摄像头采集以SDRAM作为显存进行HDMI输出显示相同,不作赘述。

时序约束

PotatoPie 4.0 实验教程(18) —— FPGA实现OV5640摄像头采集以SDRAM作为显存进行HDMI输出显示相同,不作赘述。

实验结果

由于rgb用的同一个gamma,导致画面偏紫,大家可以自行调整参数生成不同的gamma表。

20240406203758646-25056a89805797742ed598f305ac843

请登录后发表评论

    没有回复内容