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

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

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

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

其中,
是输入图像某一点的亮度值,
是输出图像上对应点的亮度值。
(1)当 0 < gamma < 1 时,图像在低灰度值区域,动态范围变大,整体图像的灰度值变大;
(2)当 gamma > 1 时,图像在高灰度值区域,动态范围变大,整体图像的灰度值变小。
使用 Matlab 进行验证,代码如下:
- clc, clear
-
- % 读取图像
- im = imread('./loopy.png');
- im = im2double(im);
-
- % gamma变换
- invgamma = 2.2;
- gamma = 1/invgamma;
- im_new = im.^gamma;
-
- subplot(121)
- imshow(im2uint8(im))
- title('原图像')
- subplot(122)
- imshow(im2uint8(im_new))
- title('处理后图像')

参考链接:Understanding Gamma Correction (cambridgeincolour.com)
使用紫光同创 FPGA 平台实现 Gamma 变换功能,FPGA 需要实现的功能与指标如下:
(1)与电脑的串口通信,用于接收上位机下发的 Gamma 曲线和原始图像,波特率为 256000 Bd/s;
(2)Gamma 变换,使用 FPGA 嵌入式 RAM,实现 Gamma 曲线的缓存与查表;
(3)DDR3 读写控制,将处理前后的图像数据分别写入 DDR3 的不同区域,实现图像的拼接;
(4)HDMI 输出,输出一路 HDMI 信号源,用于将拼接后的图像显示在外接显示器上,分辨率为 1024×768。
主要的设计模块层次与功能说明如下:
| 模块名称 | 功能说明 | |
| top_uart | uart_rx_slice | 串口接收驱动模块 |
| uart_rx_parse | 串口数据解析模块,从上位机接收 8bit 原始图像,以及 Gamma 曲线数据 | |
| top_vidin | vidin_pipeline | 缓存两行图像数据,并将数据提交到 ddr3 数据调度模块 |
| conv_gamma | Gamma 变换模块,使用 DPRAM 存储器进行 Gamma 查表 | |
| merge_out | dvi_timing_gen | HDMI 视频时序产生模块 |
| dvi_ddr_rd | 根据 HDMI 控制信号,提交读指令到 ddr3 数据调度模块 | |
| dvi_encoder | HDMI 输出编码(8b10b 编码)与输出驱动模块 | |
其中,conv_gamma 模块主要使用 dpram 查表的方式,对原始图像的 RGB 分量分别进行 Gamma 变换,模块代码如下:
- `timescale 1 ns/ 1 ps
-
- module conv_gamma (
- // System level
- sys_rst,
- sys_clk,
-
- // Gamma parameter input
- para_gamma_waddr,
- para_gamma_data,
- para_gamma_wren,
-
- // Gamma data input and output
- gamma_in_data,
- gamma_out_data
- );
-
- // IO direction/register definitions
- input sys_rst;
- input sys_clk;
- input [7:0] para_gamma_waddr;
- input [7:0] para_gamma_data;
- input para_gamma_wren;
- input [23:0] gamma_in_data;
- output [23:0] gamma_out_data;
-
- // internal signal declarations
- reg [7:0] blk_mem_waddr;
- reg [7:0] blk_mem_wdata;
- reg blk_mem_wren;
-
- // gamma_dpram_inst_r: Block dpram for gamma data buffer
- blk_mem_256x8b_gamma gamma_dpram_inst_r (
- .wr_data (blk_mem_wdata ), // input 8-bit
- .wr_addr (blk_mem_waddr ), // input 8-bit
- .wr_en (blk_mem_wren ), // input 1-bit
- .wr_clk (sys_clk ), // input 1-bit
- .wr_rst (sys_rst ), // input 1-bit
- .rd_addr (gamma_in_data[2*8+:8] ), // input 8-bit
- .rd_data (gamma_out_data[2*8+:8] ), // output 8-bit
- .rd_clk (sys_clk ), // input 1-bit
- .rd_rst (sys_rst ) // input 1-bit
- );
- // End of gamma_dpram_inst_r instantiation
-
- // gamma_dpram_inst_g: Block dpram for gamma data buffer
- blk_mem_256x8b_gamma gamma_dpram_inst_g (
- .wr_data (blk_mem_wdata ), // input 8-bit
- .wr_addr (blk_mem_waddr ), // input 8-bit
- .wr_en (blk_mem_wren ), // input 1-bit
- .wr_clk (sys_clk ), // input 1-bit
- .wr_rst (sys_rst ), // input 1-bit
- .rd_addr (gamma_in_data[1*8+:8] ), // input 8-bit
- .rd_data (gamma_out_data[1*8+:8] ), // output 8-bit
- .rd_clk (sys_clk ), // input 1-bit
- .rd_rst (sys_rst ) // input 1-bit
- );
- // End of gamma_dpram_inst_g instantiation
-
- // gamma_dpram_inst_b: Block dpram for gamma data buffer
- blk_mem_256x8b_gamma gamma_dpram_inst_b (
- .wr_data (blk_mem_wdata ), // input 8-bit
- .wr_addr (blk_mem_waddr ), // input 8-bit
- .wr_en (blk_mem_wren ), // input 1-bit
- .wr_clk (sys_clk ), // input 1-bit
- .wr_rst (sys_rst ), // input 1-bit
- .rd_addr (gamma_in_data[0*8+:8] ), // input 8-bit
- .rd_data (gamma_out_data[0*8+:8] ), // output 8-bit
- .rd_clk (sys_clk ), // input 1-bit
- .rd_rst (sys_rst ) // input 1-bit
- );
- // End of gamma_dpram_inst_b instantiation
-
- always @(posedge sys_rst or posedge sys_clk) begin
- if (sys_rst) begin
- blk_mem_waddr <= {8{1'b0}};
- blk_mem_wdata <= 8'h00;
- blk_mem_wren <= 1'b0;
- end
- else begin
- blk_mem_waddr <= para_gamma_waddr;
- blk_mem_wdata <= para_gamma_data;
- blk_mem_wren <= para_gamma_wren;
- end
- end
- endmodule
使用 PyQt5 和 OpenCV 库编写上位机程序,通过串口发送 Gamma 曲线和原始图像数据,代码如下:
- # -*- Coding: UTF-8 -*-
- import cv2
- import sys
- import struct
- import numpy as np
- import pyqtgraph as pg
- from PyQt5 import Qt, QtGui, QtCore, QtWidgets, QtSerialPort
-
- class sliderWindow(Qt.QWidget):
- def __init__(self, parent=None):
- super(sliderWindow, self).__init__(parent)
- self.setGeometry(1250, 320, 400, 400)
- self.setWindowTitle("Slider Window")
-
- # 创建绘图窗口
- self.plot_graph = pg.PlotWidget()
- self.plot_graph.setBackground('#303030')
- self.plot_graph.setXRange(0,1)
- self.plot_graph.setYRange(0,1)
- self.plot_graph.showGrid(x=True, y=True)
-
- gray = np.linspace(0, 1, 255)
- gamma = np.array(np.power(gray, 1))
- self.pen = pg.mkPen(color=(255, 255, 255), width=5, style=QtCore.Qt.SolidLine)
- self.plot_graph.plot(gray, gamma)
-
- # 创建底部滑动条
- self.label = QtWidgets.QLabel("1.00")
- self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
- self.slider.setMinimum(20)
- self.slider.setMaximum(400)
- self.slider.setValue(100)
- #self.slider.setSingleStep(1)
- self.slider.setTickInterval(10)
- self.slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
- self.slider.valueChanged.connect(self.valueChanged)
-
- bottomLayout = QtWidgets.QHBoxLayout()
- bottomLayout.addWidget(self.label)
- bottomLayout.addWidget(self.slider)
-
- # 创建中心布局
- centralLayout = QtWidgets.QVBoxLayout()
- centralLayout.addWidget(self.plot_graph)
- centralLayout.addLayout(bottomLayout)
- self.setLayout(centralLayout)
-
- def valueChanged(self):
- """更新参数值"""
- if self.slider.value() == 0:
- float_value = 0.01
- else:
- float_value = self.slider.value() /100.0
- self.label.setText("{:.2f}".format(float_value))
- self.updatePlot(float_value)
-
- def updatePlot(self, gamma):
- gray = np.linspace(0,1,255)
- gray_gamma = np.array(np.power(gray, 1/gamma))
- self.plot_graph.clear()
- self.plot_graph.plot(gray, gray_gamma)
-
- class mainWindow(Qt.QWidget):
- def __init__(self, com_port, parent=None):
- super(mainWindow, self).__init__(parent)
- self.setFixedSize(530, 384)
- self.setWindowTitle("PGL OpenCV Tool")
-
- # 创建标签与按钮
- self.img_widget = QtWidgets.QLabel()
- self.btn1 = QtWidgets.QPushButton("打开")
- self.btn1.clicked.connect(self.getfile)
- self.btn2 = QtWidgets.QPushButton("关闭")
- self.btn2.clicked.connect(self.close)
-
- # 创建布局
- centralLayout = QtWidgets.QVBoxLayout()
- centralLayout.addWidget(self.img_widget)
- bottomLayout = QtWidgets.QHBoxLayout()
- bottomLayout.addWidget(self.btn1)
- bottomLayout.addWidget(self.btn2)
- centralLayout.addLayout(bottomLayout)
- self.setLayout(centralLayout)
-
- # 串口对象
- self.COM = QtSerialPort.QSerialPort()
- self.COM.setPortName(com_port)
- self.COM.setBaudRate(256000)
- self.open_status = False
- self.row_cnt = 0
- self.img = None
- self.timer = QtCore.QTimer()
- self.timer.timeout.connect(self.sendImage)
- self.startup()
-
- def startup(self):
- """Write code here to run once"""
- self.slider_window = sliderWindow()
- self.slider_window.slider.valueChanged.connect(self.transformGamma)
- self.slider_window.slider.valueChanged.connect(self.sendGamma)
-
- for com_port in QtSerialPort.QSerialPortInfo.availablePorts():
- print(com_port.portName())
-
- # Try open serial port
- if not self.COM.open(QtSerialPort.QSerialPort.ReadWrite):
- self.open_status = False
- print("Open Serial Port failed.")
- else:
- self.open_status = True
-
- def getfile(self):
- """获取图像路径"""
- fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file',
- 'C:\\Users\\Administrator\\Pictures', "Image files(*.jpg *.png)")
- self.clipImage(fname[0])
- self.updateImage()
- self.sendImage()
-
- def clipImage(self, fname):
- """读取并裁剪图片至512x384大小"""
- if fname:
- img = cv2.imread(fname, cv2.IMREAD_COLOR)
- img_roi = img[:384,:512,:]
- print(img_roi.shape)
- cv2.imwrite('./img_roi.png', img_roi)
-
- def transformGamma(self):
- """Gamma变换"""
- if self.slider_window.slider.value() == 0:
- invgamma = 0.01
- else:
- invgamma = self.slider_window.slider.value() /100.0
-
- gamma = 1/invgamma
- img_trans = np.array(np.power(self.img/255, gamma)*255, dtype=np.uint8)
- cv2.imwrite('./img_gamma.png', img_trans)
- self.img_widget.setPixmap(QtGui.QPixmap('./img_gamma.png'))
-
- def updateImage(self):
- """显示裁剪后的图像"""
- self.img = cv2.imread('./img_roi.png')
-
- # 判断显示原图像,还是Gamma变换后的图像
- if self.slider_window.slider.value() == 100:
- self.img_widget.setPixmap(QtGui.QPixmap('./img_roi.png'))
- else:
- self.transformGamma()
-
- if self.open_status:
- self.timer.start(100)
-
- def sendImage(self):
- """通过串口发送图片"""
- pattern = ">2H{:d}B".format(512*3)
-
- if self.open_status:
- if self.row_cnt == 384:
- self.row_cnt = 0
- self.timer.stop()
- else:
- args1 = [0x5500, self.row_cnt]
- args2 = [rgb for rgb in self.img[self.row_cnt,:].reshape(-1)]
- send_data = struct.pack(pattern, *(args1+args2))
- self.row_cnt += 1
- self.COM.write(send_data)
-
- def sendGamma(self):
- """通过串口发送Gamma曲线"""
- if self.slider_window.slider.value() == 0:
- invgamma = 0.01
- else:
- invgamma = self.slider_window.slider.value() /100.0
-
- gamma = 1/invgamma
- gamma_f = lambda x: np.uint8(np.floor(np.power(x/255, gamma)*255))
- pattern = ">1H{:d}B".format(256)
-
- if self.open_status:
- args1 = [0xAA00]
- args2 = [gamma_f(x) for x in range(256)]
- send_data = struct.pack(pattern, *(args1+args2))
- self.COM.write(send_data)
-
- def closeEvent(self, event):
- super().closeEvent(event)
- self.slider_window.close() # 关闭子窗口
-
- # 定时器停止
- self.timer.stop()
- if self.open_status:
- self.COM.close() # 关闭串口
-
- def main():
- app = QtWidgets.QApplication(sys.argv)
- window = mainWindow('COM21')
- for win in (window, window.slider_window):
- win.show()
- sys.exit(app.exec_())
-
- if __name__ == "__main__":
- main()

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