• 【数字图像处理】Gamma 变换


    在数字图像处理中,Gamma 变换是一种重要的灰度变换方法,可以用于图像增强与 Gamma 校正。本文主要介绍数字图像 Gamma 变换的基本原理,并记录在紫光同创 PGL22G FPGA 平台的布署与实现过程。

    目录

    1. Gamma 变换原理

    2. FPGA 布署与实现

    2.1 功能与指标定义

    2.2 模块设计

    2.3 上板调试


    1. Gamma 变换原理

            在摄像机成像过程中,人们使用了 Gamma 编码对图像进行处理,这样做的好处是能更好地记录与存储图像。

            采用 Gamma 编码的图像在显示器上显示时,需要进行 Gamma 校正,以还原图像。

    Gamma 校正可以用以下变换公式表示:

    V_{out} = (V_{in})^{gamma}

    其中,V_{in} 是输入图像某一点的亮度值,V_{out} 是输出图像上对应点的亮度值。

    (1)当 0 < gamma < 1 时,图像在低灰度值区域,动态范围变大,整体图像的灰度值变大;

    (2)当 gamma > 1 时,图像在高灰度值区域,动态范围变大,整体图像的灰度值变小。

    使用 Matlab 进行验证,代码如下:

    1. clc, clear
    2. % 读取图像
    3. im = imread('./loopy.png');
    4. im = im2double(im);
    5. % gamma变换
    6. invgamma = 2.2;
    7. gamma = 1/invgamma;
    8. im_new = im.^gamma;
    9. subplot(121)
    10. imshow(im2uint8(im))
    11. title('原图像')
    12. subplot(122)
    13. imshow(im2uint8(im_new))
    14. title('处理后图像')

    参考链接:Understanding Gamma Correction (cambridgeincolour.com)

    2. FPGA 布署与实现

    2.1 功能与指标定义

            使用紫光同创 FPGA 平台实现 Gamma 变换功能,FPGA 需要实现的功能与指标如下:

    (1)与电脑的串口通信,用于接收上位机下发的 Gamma 曲线和原始图像,波特率为 256000 Bd/s;

    (2)Gamma 变换,使用 FPGA 嵌入式 RAM,实现 Gamma 曲线的缓存与查表;

    (3)DDR3 读写控制,将处理前后的图像数据分别写入 DDR3 的不同区域,实现图像的拼接;

    (4)HDMI 输出,输出一路 HDMI 信号源,用于将拼接后的图像显示在外接显示器上,分辨率为 1024×768。

    2.2 模块设计

            主要的设计模块层次与功能说明如下:

    模块名称功能说明
    top_uartuart_rx_slice串口接收驱动模块
    uart_rx_parse串口数据解析模块,从上位机接收 8bit 原始图像,以及 Gamma 曲线数据
    top_vidinvidin_pipeline缓存两行图像数据,并将数据提交到 ddr3 数据调度模块
    conv_gammaGamma 变换模块,使用 DPRAM 存储器进行 Gamma 查表
    merge_outdvi_timing_genHDMI 视频时序产生模块
    dvi_ddr_rd根据 HDMI 控制信号,提交读指令到 ddr3 数据调度模块
    dvi_encoderHDMI 输出编码(8b10b 编码)与输出驱动模块

            其中,conv_gamma 模块主要使用 dpram 查表的方式,对原始图像的 RGB 分量分别进行 Gamma 变换,模块代码如下:

    1. `timescale 1 ns/ 1 ps
    2. module conv_gamma (
    3. // System level
    4. sys_rst,
    5. sys_clk,
    6. // Gamma parameter input
    7. para_gamma_waddr,
    8. para_gamma_data,
    9. para_gamma_wren,
    10. // Gamma data input and output
    11. gamma_in_data,
    12. gamma_out_data
    13. );
    14. // IO direction/register definitions
    15. input sys_rst;
    16. input sys_clk;
    17. input [7:0] para_gamma_waddr;
    18. input [7:0] para_gamma_data;
    19. input para_gamma_wren;
    20. input [23:0] gamma_in_data;
    21. output [23:0] gamma_out_data;
    22. // internal signal declarations
    23. reg [7:0] blk_mem_waddr;
    24. reg [7:0] blk_mem_wdata;
    25. reg blk_mem_wren;
    26. // gamma_dpram_inst_r: Block dpram for gamma data buffer
    27. blk_mem_256x8b_gamma gamma_dpram_inst_r (
    28. .wr_data (blk_mem_wdata ), // input 8-bit
    29. .wr_addr (blk_mem_waddr ), // input 8-bit
    30. .wr_en (blk_mem_wren ), // input 1-bit
    31. .wr_clk (sys_clk ), // input 1-bit
    32. .wr_rst (sys_rst ), // input 1-bit
    33. .rd_addr (gamma_in_data[2*8+:8] ), // input 8-bit
    34. .rd_data (gamma_out_data[2*8+:8] ), // output 8-bit
    35. .rd_clk (sys_clk ), // input 1-bit
    36. .rd_rst (sys_rst ) // input 1-bit
    37. );
    38. // End of gamma_dpram_inst_r instantiation
    39. // gamma_dpram_inst_g: Block dpram for gamma data buffer
    40. blk_mem_256x8b_gamma gamma_dpram_inst_g (
    41. .wr_data (blk_mem_wdata ), // input 8-bit
    42. .wr_addr (blk_mem_waddr ), // input 8-bit
    43. .wr_en (blk_mem_wren ), // input 1-bit
    44. .wr_clk (sys_clk ), // input 1-bit
    45. .wr_rst (sys_rst ), // input 1-bit
    46. .rd_addr (gamma_in_data[1*8+:8] ), // input 8-bit
    47. .rd_data (gamma_out_data[1*8+:8] ), // output 8-bit
    48. .rd_clk (sys_clk ), // input 1-bit
    49. .rd_rst (sys_rst ) // input 1-bit
    50. );
    51. // End of gamma_dpram_inst_g instantiation
    52. // gamma_dpram_inst_b: Block dpram for gamma data buffer
    53. blk_mem_256x8b_gamma gamma_dpram_inst_b (
    54. .wr_data (blk_mem_wdata ), // input 8-bit
    55. .wr_addr (blk_mem_waddr ), // input 8-bit
    56. .wr_en (blk_mem_wren ), // input 1-bit
    57. .wr_clk (sys_clk ), // input 1-bit
    58. .wr_rst (sys_rst ), // input 1-bit
    59. .rd_addr (gamma_in_data[0*8+:8] ), // input 8-bit
    60. .rd_data (gamma_out_data[0*8+:8] ), // output 8-bit
    61. .rd_clk (sys_clk ), // input 1-bit
    62. .rd_rst (sys_rst ) // input 1-bit
    63. );
    64. // End of gamma_dpram_inst_b instantiation
    65. always @(posedge sys_rst or posedge sys_clk) begin
    66. if (sys_rst) begin
    67. blk_mem_waddr <= {8{1'b0}};
    68. blk_mem_wdata <= 8'h00;
    69. blk_mem_wren <= 1'b0;
    70. end
    71. else begin
    72. blk_mem_waddr <= para_gamma_waddr;
    73. blk_mem_wdata <= para_gamma_data;
    74. blk_mem_wren <= para_gamma_wren;
    75. end
    76. end
    77. endmodule

    2.3 上板调试

            使用 PyQt5 和 OpenCV 库编写上位机程序,通过串口发送 Gamma 曲线和原始图像数据,代码如下:

    1. # -*- Coding: UTF-8 -*-
    2. import cv2
    3. import sys
    4. import struct
    5. import numpy as np
    6. import pyqtgraph as pg
    7. from PyQt5 import Qt, QtGui, QtCore, QtWidgets, QtSerialPort
    8. class sliderWindow(Qt.QWidget):
    9. def __init__(self, parent=None):
    10. super(sliderWindow, self).__init__(parent)
    11. self.setGeometry(1250, 320, 400, 400)
    12. self.setWindowTitle("Slider Window")
    13. # 创建绘图窗口
    14. self.plot_graph = pg.PlotWidget()
    15. self.plot_graph.setBackground('#303030')
    16. self.plot_graph.setXRange(0,1)
    17. self.plot_graph.setYRange(0,1)
    18. self.plot_graph.showGrid(x=True, y=True)
    19. gray = np.linspace(0, 1, 255)
    20. gamma = np.array(np.power(gray, 1))
    21. self.pen = pg.mkPen(color=(255, 255, 255), width=5, style=QtCore.Qt.SolidLine)
    22. self.plot_graph.plot(gray, gamma)
    23. # 创建底部滑动条
    24. self.label = QtWidgets.QLabel("1.00")
    25. self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
    26. self.slider.setMinimum(20)
    27. self.slider.setMaximum(400)
    28. self.slider.setValue(100)
    29. #self.slider.setSingleStep(1)
    30. self.slider.setTickInterval(10)
    31. self.slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
    32. self.slider.valueChanged.connect(self.valueChanged)
    33. bottomLayout = QtWidgets.QHBoxLayout()
    34. bottomLayout.addWidget(self.label)
    35. bottomLayout.addWidget(self.slider)
    36. # 创建中心布局
    37. centralLayout = QtWidgets.QVBoxLayout()
    38. centralLayout.addWidget(self.plot_graph)
    39. centralLayout.addLayout(bottomLayout)
    40. self.setLayout(centralLayout)
    41. def valueChanged(self):
    42. """更新参数值"""
    43. if self.slider.value() == 0:
    44. float_value = 0.01
    45. else:
    46. float_value = self.slider.value() /100.0
    47. self.label.setText("{:.2f}".format(float_value))
    48. self.updatePlot(float_value)
    49. def updatePlot(self, gamma):
    50. gray = np.linspace(0,1,255)
    51. gray_gamma = np.array(np.power(gray, 1/gamma))
    52. self.plot_graph.clear()
    53. self.plot_graph.plot(gray, gray_gamma)
    54. class mainWindow(Qt.QWidget):
    55. def __init__(self, com_port, parent=None):
    56. super(mainWindow, self).__init__(parent)
    57. self.setFixedSize(530, 384)
    58. self.setWindowTitle("PGL OpenCV Tool")
    59. # 创建标签与按钮
    60. self.img_widget = QtWidgets.QLabel()
    61. self.btn1 = QtWidgets.QPushButton("打开")
    62. self.btn1.clicked.connect(self.getfile)
    63. self.btn2 = QtWidgets.QPushButton("关闭")
    64. self.btn2.clicked.connect(self.close)
    65. # 创建布局
    66. centralLayout = QtWidgets.QVBoxLayout()
    67. centralLayout.addWidget(self.img_widget)
    68. bottomLayout = QtWidgets.QHBoxLayout()
    69. bottomLayout.addWidget(self.btn1)
    70. bottomLayout.addWidget(self.btn2)
    71. centralLayout.addLayout(bottomLayout)
    72. self.setLayout(centralLayout)
    73. # 串口对象
    74. self.COM = QtSerialPort.QSerialPort()
    75. self.COM.setPortName(com_port)
    76. self.COM.setBaudRate(256000)
    77. self.open_status = False
    78. self.row_cnt = 0
    79. self.img = None
    80. self.timer = QtCore.QTimer()
    81. self.timer.timeout.connect(self.sendImage)
    82. self.startup()
    83. def startup(self):
    84. """Write code here to run once"""
    85. self.slider_window = sliderWindow()
    86. self.slider_window.slider.valueChanged.connect(self.transformGamma)
    87. self.slider_window.slider.valueChanged.connect(self.sendGamma)
    88. for com_port in QtSerialPort.QSerialPortInfo.availablePorts():
    89. print(com_port.portName())
    90. # Try open serial port
    91. if not self.COM.open(QtSerialPort.QSerialPort.ReadWrite):
    92. self.open_status = False
    93. print("Open Serial Port failed.")
    94. else:
    95. self.open_status = True
    96. def getfile(self):
    97. """获取图像路径"""
    98. fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file',
    99. 'C:\\Users\\Administrator\\Pictures', "Image files(*.jpg *.png)")
    100. self.clipImage(fname[0])
    101. self.updateImage()
    102. self.sendImage()
    103. def clipImage(self, fname):
    104. """读取并裁剪图片至512x384大小"""
    105. if fname:
    106. img = cv2.imread(fname, cv2.IMREAD_COLOR)
    107. img_roi = img[:384,:512,:]
    108. print(img_roi.shape)
    109. cv2.imwrite('./img_roi.png', img_roi)
    110. def transformGamma(self):
    111. """Gamma变换"""
    112. if self.slider_window.slider.value() == 0:
    113. invgamma = 0.01
    114. else:
    115. invgamma = self.slider_window.slider.value() /100.0
    116. gamma = 1/invgamma
    117. img_trans = np.array(np.power(self.img/255, gamma)*255, dtype=np.uint8)
    118. cv2.imwrite('./img_gamma.png', img_trans)
    119. self.img_widget.setPixmap(QtGui.QPixmap('./img_gamma.png'))
    120. def updateImage(self):
    121. """显示裁剪后的图像"""
    122. self.img = cv2.imread('./img_roi.png')
    123. # 判断显示原图像,还是Gamma变换后的图像
    124. if self.slider_window.slider.value() == 100:
    125. self.img_widget.setPixmap(QtGui.QPixmap('./img_roi.png'))
    126. else:
    127. self.transformGamma()
    128. if self.open_status:
    129. self.timer.start(100)
    130. def sendImage(self):
    131. """通过串口发送图片"""
    132. pattern = ">2H{:d}B".format(512*3)
    133. if self.open_status:
    134. if self.row_cnt == 384:
    135. self.row_cnt = 0
    136. self.timer.stop()
    137. else:
    138. args1 = [0x5500, self.row_cnt]
    139. args2 = [rgb for rgb in self.img[self.row_cnt,:].reshape(-1)]
    140. send_data = struct.pack(pattern, *(args1+args2))
    141. self.row_cnt += 1
    142. self.COM.write(send_data)
    143. def sendGamma(self):
    144. """通过串口发送Gamma曲线"""
    145. if self.slider_window.slider.value() == 0:
    146. invgamma = 0.01
    147. else:
    148. invgamma = self.slider_window.slider.value() /100.0
    149. gamma = 1/invgamma
    150. gamma_f = lambda x: np.uint8(np.floor(np.power(x/255, gamma)*255))
    151. pattern = ">1H{:d}B".format(256)
    152. if self.open_status:
    153. args1 = [0xAA00]
    154. args2 = [gamma_f(x) for x in range(256)]
    155. send_data = struct.pack(pattern, *(args1+args2))
    156. self.COM.write(send_data)
    157. def closeEvent(self, event):
    158. super().closeEvent(event)
    159. self.slider_window.close() # 关闭子窗口
    160. # 定时器停止
    161. self.timer.stop()
    162. if self.open_status:
    163. self.COM.close() # 关闭串口
    164. def main():
    165. app = QtWidgets.QApplication(sys.argv)
    166. window = mainWindow('COM21')
    167. for win in (window, window.slider_window):
    168. win.show()
    169. sys.exit(app.exec_())
    170. if __name__ == "__main__":
    171. main()

    连接串口线与 HDMI 线,拖动滑动条改变 Gamma 值,上位机程序会自动发送 Gamma 曲线到开发板,然后发送要显示的图像,就可以看到 FPGA 处理的效果了 ~

  • 相关阅读:
    05 CSS02
    02_openstack私有云部署
    SpringBoot2.7.9 配置文件加载方式
    java计算机毕业设计医院人事档案管理系源代码+系统+数据库+lw文档
    应对铜价飙升,慧能泰推出超高性价比240W五芯线专用eMarker芯片
    测开(自动化测试selenium(WebDriver API))
    读书笔记: 这就是搜索引擎
    深度学习修炼(二)全连接神经网络 | Softmax,交叉熵损失函数 优化AdaGrad,RMSProp等 对抗过拟合 全攻略
    day60
    数说故事以领先的大数据分析及服务实践,实力入围「Cloud 100 China 2022」首届榜单
  • 原文地址:https://blog.csdn.net/sxyang2018/article/details/134489391