本原创文章由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com)
1. 实验简介
实验目的:
生成 3x3 矩阵,完成高斯滤波。
实验环境:
Window11
PDS2022.SP6.4
Modelsim10.6c
MatlabR2023b
硬件环境:
MES2L676-100HP
2. 实验原理
Gamma 校正是基于人眼对亮度感知非线性的特性,人眼对亮度的敏感度随着亮度的增加而减少,也就是说,当图像的亮度较低时,人眼对亮度的变化更为敏感。
具体的公式为:
x 大于 1 时,图像变暗。
x 小于 1 时,图像变亮。
x 等于 1 时,图像不变。
同样的,我们会发现一个问题,该公式涉及到指数和除法,用 verilog 来实现的难度是比较大的,我们可以换个角度思考,本次的亮度调整的像素是 8bit,因此是 0-255,我们可以通过查找表的 方法来完成本次算法,x 是变量,是用来调节亮度的参数,Vin 的输入范围是 0-255,假设,x 为 2.2,我们可以通 过 Matlab 用 for 循环遍历 0-255,然后计算出当 x=2.2 的时候,0-255 中的每个数的输出值是多少,提前计算 出来,做成查找表,在 verilog 中,我们就可以通过 case 语句来完成本次算法,大大降低了运算的复杂度。具体 实现过程可以看后续的 Matlab 仿真部分。
3. 接口列表
gamma_lookuip.v 模块接口
因为做成了查找表,所以接口非常简单,仅有输入像素数据和输出像素数据。
4. 工程说明
本次工程可以直接对输入进来的 RGB888 数据进行 gamma 矫正提高亮度,或者先进行灰度化后,再进行 gamma 矫正都可以。对于 RGB 的图像,需要先分出 R、G、B 三个通道,每个通道均为 8bit,然后例化三个 gamma 矫正模块即可。
4.1. 代码模块说明
主要介绍 gamma_lookuptable 模块。
// gamma_table 1/2.2
module gamma_lookuptable
(
input [7:0] video_data,
output reg [7:0] gamma_data
);
always @(*)
begin
case(video_data)
8'd0 : gamma_data = 8'd0;
8'd1 : gamma_data = 8'd21;
8'd2 : gamma_data = 8'd28;
8'd3 : gamma_data = 8'd34;
8'd4 : gamma_data = 8'd39;
8'd5 : gamma_data = 8'd43;
8'd6 : gamma_data = 8'd46;
8'd7 : gamma_data = 8'd50;
8'd8 : gamma_data = 8'd53;
8'd9 : gamma_data = 8'd56;
8'd10 : gamma_data = 8'd59;
8'd11 : gamma_data = 8'd61;
8'd12 : gamma_data = 8'd64;
8'd13 : gamma_data = 8'd66;
8'd14 : gamma_data = 8'd68;
8'd15 : gamma_data = 8'd70;
8'd16 : gamma_data = 8'd72;
8'd17 : gamma_data = 8'd74;
8'd18 : gamma_data = 8'd76;
8'd19 : gamma_data = 8'd78;
8'd20 : gamma_data = 8'd80;
8'd21 : gamma_data = 8'd82;
8'd22 : gamma_data = 8'd84;
8'd23 : gamma_data = 8'd85;
8'd24 : gamma_data = 8'd87;
8'd25 : gamma_data = 8'd89;
8'd26 : gamma_data = 8'd90;
8'd27 : gamma_data = 8'd92;
8'd28 : gamma_data = 8'd93;
8'd29 : gamma_data = 8'd95;
8'd30 : gamma_data = 8'd96;
8'd31 : gamma_data = 8'd98;
8'd32 : gamma_data = 8'd99;
8'd33 : gamma_data = 8'd101;
8'd34 : gamma_data = 8'd102;
8'd35 : gamma_data = 8'd103;
8'd36 : gamma_data = 8'd105;
8'd37 : gamma_data = 8'd106;
8'd38 : gamma_data = 8'd107;
8'd39 : gamma_data = 8'd109;
8'd40 : gamma_data = 8'd110;
8'd41 : gamma_data = 8'd111;
8'd42 : gamma_data = 8'd112;
8'd43 : gamma_data = 8'd114;
8'd44 : gamma_data = 8'd115;
8'd45 : gamma_data = 8'd116;
8'd46 : gamma_data = 8'd117;
8'd47 : gamma_data = 8'd118;
8'd48 : gamma_data = 8'd119;
8'd49 : gamma_data = 8'd120;
8'd50 : gamma_data = 8'd122;
8'd51 : gamma_data = 8'd123;
8'd52 : gamma_data = 8'd124;
8'd53 : gamma_data = 8'd125;
8'd54 : gamma_data = 8'd126;
8'd55 : gamma_data = 8'd127;
8'd56 : gamma_data = 8'd128;
8'd57 : gamma_data = 8'd129;
8'd58 : gamma_data = 8'd130;
8'd59 : gamma_data = 8'd131;
8'd60 : gamma_data = 8'd132;
8'd61 : gamma_data = 8'd133;
8'd62 : gamma_data = 8'd134;
8'd63 : gamma_data = 8'd135;
8'd64 : gamma_data = 8'd136;
8'd65 : gamma_data = 8'd137;
8'd66 : gamma_data = 8'd138;
8'd67 : gamma_data = 8'd139;
8'd68 : gamma_data = 8'd140;
8'd69 : gamma_data = 8'd141;
8'd70 : gamma_data = 8'd142;
8'd71 : gamma_data = 8'd143;
8'd72 : gamma_data = 8'd144;
8'd73 : gamma_data = 8'd144;
8'd74 : gamma_data = 8'd145;
8'd75 : gamma_data = 8'd146;
8'd76 : gamma_data = 8'd147;
8'd77 : gamma_data = 8'd148;
8'd78 : gamma_data = 8'd149;
8'd79 : gamma_data = 8'd150;
8'd80 : gamma_data = 8'd151;
8'd81 : gamma_data = 8'd151;
8'd82 : gamma_data = 8'd152;
8'd83 : gamma_data = 8'd153;
8'd84 : gamma_data = 8'd154;
8'd85 : gamma_data = 8'd155;
8'd86 : gamma_data = 8'd156;
8'd87 : gamma_data = 8'd156;
8'd88 : gamma_data = 8'd157;
8'd89 : gamma_data = 8'd158;
8'd90 : gamma_data = 8'd159;
8'd91 : gamma_data = 8'd160;
8'd92 : gamma_data = 8'd160;
8'd93 : gamma_data = 8'd161;
8'd94 : gamma_data = 8'd162;
8'd95 : gamma_data = 8'd163;
8'd96 : gamma_data = 8'd164;
8'd97 : gamma_data = 8'd164;
8'd98 : gamma_data = 8'd165;
8'd99 : gamma_data = 8'd166;
8'd100 : gamma_data = 8'd167;
8'd101 : gamma_data = 8'd167;
8'd102 : gamma_data = 8'd168;
8'd103 : gamma_data = 8'd169;
8'd104 : gamma_data = 8'd170;
8'd105 : gamma_data = 8'd170;
8'd106 : gamma_data = 8'd171;
8'd107 : gamma_data = 8'd172;
8'd108 : gamma_data = 8'd173;
8'd109 : gamma_data = 8'd173;
8'd110 : gamma_data = 8'd174;
8'd111 : gamma_data = 8'd175;
8'd112 : gamma_data = 8'd175;
8'd113 : gamma_data = 8'd176;
8'd114 : gamma_data = 8'd177;
8'd115 : gamma_data = 8'd178;
8'd116 : gamma_data = 8'd178;
8'd117 : gamma_data = 8'd179;
8'd118 : gamma_data = 8'd180;
8'd119 : gamma_data = 8'd180;
8'd120 : gamma_data = 8'd181;
8'd121 : gamma_data = 8'd182;
8'd122 : gamma_data = 8'd182;
8'd123 : gamma_data = 8'd183;
8'd124 : gamma_data = 8'd184;
8'd125 : gamma_data = 8'd184;
8'd126 : gamma_data = 8'd185;
8'd127 : gamma_data = 8'd186;
8'd128 : gamma_data = 8'd186;
8'd129 : gamma_data = 8'd187;
8'd130 : gamma_data = 8'd188;
8'd131 : gamma_data = 8'd188;
8'd132 : gamma_data = 8'd189;
8'd133 : gamma_data = 8'd190;
8'd134 : gamma_data = 8'd190;
8'd135 : gamma_data = 8'd191;
8'd136 : gamma_data = 8'd192;
8'd137 : gamma_data = 8'd192;
8'd138 : gamma_data = 8'd193;
8'd139 : gamma_data = 8'd194;
8'd140 : gamma_data = 8'd194;
8'd141 : gamma_data = 8'd195;
8'd142 : gamma_data = 8'd195;
8'd143 : gamma_data = 8'd196;
8'd144 : gamma_data = 8'd197;
8'd145 : gamma_data = 8'd197;
8'd146 : gamma_data = 8'd198;
8'd147 : gamma_data = 8'd199;
8'd148 : gamma_data = 8'd199;
8'd149 : gamma_data = 8'd200;
8'd150 : gamma_data = 8'd200;
8'd151 : gamma_data = 8'd201;
8'd152 : gamma_data = 8'd202;
8'd153 : gamma_data = 8'd202;
8'd154 : gamma_data = 8'd203;
8'd155 : gamma_data = 8'd203;
8'd156 : gamma_data = 8'd204;
8'd157 : gamma_data = 8'd205;
8'd158 : gamma_data = 8'd205;
8'd159 : gamma_data = 8'd206;
8'd160 : gamma_data = 8'd206;
8'd161 : gamma_data = 8'd207;
8'd162 : gamma_data = 8'd207;
8'd163 : gamma_data = 8'd208;
8'd164 : gamma_data = 8'd209;
8'd165 : gamma_data = 8'd209;
8'd166 : gamma_data = 8'd210;
8'd167 : gamma_data = 8'd210;
8'd168 : gamma_data = 8'd211;
8'd169 : gamma_data = 8'd212;
8'd170 : gamma_data = 8'd212;
8'd171 : gamma_data = 8'd213;
8'd172 : gamma_data = 8'd213;
8'd173 : gamma_data = 8'd214;
8'd174 : gamma_data = 8'd214;
8'd175 : gamma_data = 8'd215;
8'd176 : gamma_data = 8'd215;
8'd177 : gamma_data = 8'd216;
8'd178 : gamma_data = 8'd217;
8'd179 : gamma_data = 8'd217;
8'd180 : gamma_data = 8'd218;
8'd181 : gamma_data = 8'd218;
8'd182 : gamma_data = 8'd219;
8'd183 : gamma_data = 8'd219;
8'd184 : gamma_data = 8'd220;
8'd185 : gamma_data = 8'd220;
8'd186 : gamma_data = 8'd221;
8'd187 : gamma_data = 8'd221;
8'd188 : gamma_data = 8'd222;
8'd189 : gamma_data = 8'd223;
8'd190 : gamma_data = 8'd223;
8'd191 : gamma_data = 8'd224;
8'd192 : gamma_data = 8'd224;
8'd193 : gamma_data = 8'd225;
8'd194 : gamma_data = 8'd225;
8'd195 : gamma_data = 8'd226;
8'd196 : gamma_data = 8'd226;
8'd197 : gamma_data = 8'd227;
8'd198 : gamma_data = 8'd227;
8'd199 : gamma_data = 8'd228;
8'd200 : gamma_data = 8'd228;
8'd201 : gamma_data = 8'd229;
8'd202 : gamma_data = 8'd229;
8'd203 : gamma_data = 8'd230;
8'd204 : gamma_data = 8'd230;
8'd205 : gamma_data = 8'd231;
8'd206 : gamma_data = 8'd231;
8'd207 : gamma_data = 8'd232;
8'd208 : gamma_data = 8'd232;
8'd209 : gamma_data = 8'd233;
8'd210 : gamma_data = 8'd233;
8'd211 : gamma_data = 8'd234;
8'd212 : gamma_data = 8'd234;
8'd213 : gamma_data = 8'd235;
8'd214 : gamma_data = 8'd235;
8'd215 : gamma_data = 8'd236;
8'd216 : gamma_data = 8'd236;
8'd217 : gamma_data = 8'd237;
8'd218 : gamma_data = 8'd237;
8'd219 : gamma_data = 8'd238;
8'd220 : gamma_data = 8'd238;
8'd221 : gamma_data = 8'd239;
8'd222 : gamma_data = 8'd239;
8'd223 : gamma_data = 8'd240;
8'd224 : gamma_data = 8'd240;
8'd225 : gamma_data = 8'd241;
8'd226 : gamma_data = 8'd241;
8'd227 : gamma_data = 8'd242;
8'd228 : gamma_data = 8'd242;
8'd229 : gamma_data = 8'd243;
8'd230 : gamma_data = 8'd243;
8'd231 : gamma_data = 8'd244;
8'd232 : gamma_data = 8'd244;
8'd233 : gamma_data = 8'd245;
8'd234 : gamma_data = 8'd245;
8'd235 : gamma_data = 8'd246;
8'd236 : gamma_data = 8'd246;
8'd237 : gamma_data = 8'd247;
8'd238 : gamma_data = 8'd247;
8'd239 : gamma_data = 8'd248;
8'd240 : gamma_data = 8'd248;
8'd241 : gamma_data = 8'd249;
8'd242 : gamma_data = 8'd249;
8'd243 : gamma_data = 8'd249;
8'd244 : gamma_data = 8'd250;
8'd245 : gamma_data = 8'd250;
8'd246 : gamma_data = 8'd251;
8'd247 : gamma_data = 8'd251;
8'd248 : gamma_data = 8'd252;
8'd249 : gamma_data = 8'd252;
8'd250 : gamma_data = 8'd253;
8'd251 : gamma_data = 8'd253;
8'd252 : gamma_data = 8'd254;
8'd253 : gamma_data = 8'd254;
8'd254 : gamma_data = 8'd255;
8'd255 : gamma_data = 8'd255;
endcase
end
endmodule
该代码其实均有 matlab 脚本自动生成。代码整体不难,通过查找表的方法找到输入的像素数据对应 gamma 处理后的值,然后输出即可。需要注意的是 always@(*)表示的是组合逻辑,所以并不会延迟一个时钟周期,故不需要对数据有效信号打拍。
4.2. 代码仿真
4.2.1. Matlab 仿真介绍
%% 清空
clc; clear all;
%% 读取图像
originalImage = imread('night1.bmp');
%% 读取 RGB 三通道
R = originalImage(:,:,1);
G = originalImage(:,:,2);
B = originalImage(:,:,3);
%% 设定 gamma 值
gamma_value = 1/2.2;
figure;
%% 显示原图
subplot(1, 2, 1);
imshow(originalImage);
title('原图像');
%% 图像尺寸
[img_height, img_width, channel] = size(originalImage);
gamma_r = zeros(img_height, img_width);
gamma_g = zeros(img_height, img_width);
gamma_b = zeros(img_height, img_width);
%% 对R通道进行gamma校正
for i = 1:img_height
for j = 1:img_width
gamma_r(i,j) = (255/255.^(gamma_value)) * double(R(i,j)).^(gamma_value);
end
end
%% 对G通道进行gamma校正
for i = 1:img_height
for j = 1:img_width
gamma_g(i,j) = (255/255.^(gamma_value)) * double(G(i,j)).^(gamma_value);
end
end
%% 对B通道进行gamma校正
for i = 1:img_height
for j = 1:img_width
gamma_b(i,j) = (255/255.^(gamma_value)) * double(B(i,j)).^(gamma_value);
end
end
%% 合并通道并显示结果
gamma_img = cat(3, uint8(gamma_r), uint8(gamma_g), uint8(gamma_b));
subplot(1, 2, 2);
imshow(gamma_img);
imwrite(gamma_img, 'D:\pango_isp\img_test_pg\img_test_pg\01_led_test\sim\matlab_gamma.png');
title('gamma 矫正后的图像');
重复的操作将不在介绍,只介绍关键部分。
在代码的 6-8 行,分离图片的 RGB 三通道。(:,:,1)表示取出红色分量。
代码的 20-22 行定义 gamma 变化后的三个通道的二维矩阵,让其大小和我们读取的图像的大小一致。
代码 24-43 行通过 for 循环分别对 RGB 三通道里的每个像素进行 gamma 变化,按照公式进行计算。
代码的 45 行将 gamma 变化后的结果强制转为 uint8 格式,然后利用 cat 函数,将三通道重新拼成一个三维图像矩阵。
最后通过 imshow(gamma_img)将 gamma 变化后的结果显示出来,imwrite 用来将图片保存到本地。
运行结果如下所示:

4.2.2. Matlab 生成查找表
%% 清空
clear all; close all; clc;
%% 设置gamma值
gamma_value = 1/2.2;
%% 打开文件
% 创建并写入gamma查找表Verilog文件
fpga_gamma = fopen('gamma_table.v', 'w');
%% 写入模块头部
fprintf(fpga_gamma, '// gamma_table 1/%.1f\n', gamma_value);
fprintf(fpga_gamma, 'module gamma_lookuptable\n');
fprintf(fpga_gamma, '(\n');
fprintf(fpga_gamma, ' input\t\t[7:0]\tvideo_data,\n');
fprintf(fpga_gamma, ' output\treg\t[7:0]\tgamma_data\n');
fprintf(fpga_gamma, ');\n\n');
%% 写入always块开始
fprintf(fpga_gamma, 'always @(*) begin\n');
fprintf(fpga_gamma, ' case(video_data)\n');
%% 计算并写入256个gamma值
gamma_array = zeros(1, 256);
for i = 1:256
gamma_array(1, i) = (255/255.^gamma_value) * (i-1).^gamma_value;
gamma_array(1, i) = uint8(gamma_array(1, i));
fprintf(fpga_gamma, ' 8''d%-3d : gamma_data = 8''d%-3d;\n', i-1, gamma_array(1, i));
end
%% 写入模块尾部
fprintf(fpga_gamma, ' endcase\n');
fprintf(fpga_gamma, 'end\n\n');
fprintf(fpga_gamma, 'endmodule\n');
%% 关闭文件
fclose(fpga_gamma);
其实这样的脚本我们可以通过 AI 工具来快速生成,提高开发效率,例如使用 GPT、文心一言等,都可以快速生成类似脚本。
fprintf()函数会在打开的文本里写入数据,'\n'表示换行,所以按照我们的 verilog 的语法来一步一步将数 据写入即可。
代码的 6-15 行就是定义了模块的基本框架比如端口等。
代码的 18-23 行,通过 for 循环遍历 0-255 共 256 个像素,把每个像素经过 gamma 变化后的结果写入到 文本之中。按照十进制写入,'\t'表示空四个格。
最后在末尾写入 endcase、end 以及 endmodule 即可。
下面便是通过 Matlab 脚本生成的查找表。
// gamma_table 1/2.2
module gamma_lookuptable (
input [7:0] video_data,
output reg [7:0] gamma_data
);
always @(*) begin
case(video_data)
8'd0 : gamma_data = 8'd0;
8'd1 : gamma_data = 8'd21;
8'd2 : gamma_data = 8'd28;
8'd3 : gamma_data = 8'd34;
8'd4 : gamma_data = 8'd39;
8'd5 : gamma_data = 8'd43;
8'd6 : gamma_data = 8'd46;
8'd7 : gamma_data = 8'd50;
8'd8 : gamma_data = 8'd53;
8'd9 : gamma_data = 8'd56;
8'd10 : gamma_data = 8'd59;
8'd11 : gamma_data = 8'd61;
8'd12 : gamma_data = 8'd64;
8'd13 : gamma_data = 8'd66;
8'd14 : gamma_data = 8'd68;
8'd15 : gamma_data = 8'd70;
8'd16 : gamma_data = 8'd72;
8'd17 : gamma_data = 8'd74;
8'd18 : gamma_data = 8'd76;
8'd19 : gamma_data = 8'd78;
......................
省略中间部分
......................
8'd255 : gamma_data = 8'd255;
endcase
end
endmodule
4.2.3. Modelsim 仿真介绍
仿真的 TB 只需要例化三个 gamma 模块,将 RGB 分出三通道,每个通道都是 8bit。具体如下所示:
gamma_lookuptable u_gamma_lookuptable_r (
.video_data (video_data[23:16]),
.gamma_data (gamma_data_r)
);
gamma_lookuptable u_gamma_lookuptable_g (
.video_data (video_data[15:8]),
.gamma_data (gamma_data_g)
);
gamma_lookuptable u_gamma_lookuptable_b (
.video_data (video_data[7:0]),
.gamma_data (gamma_data_b)
);
将 RGB888 分别给到 gamma 模块进行处理即可。
always @(posedge video_clk or negedge rst_n) begin
if (!rst_n)
video_vs_d <= 1'd0;
else
video_vs_d <= video_vs;
end
assign frame_flag = ~video_vs & video_vs_d; // 下降沿检测
always @(posedge video_clk or negedge rst_n) begin
if (!rst_n)
img_done <= 1'b0;
else if (frame_flag) // 下降沿判断一帧结束
img_done <= 1'b1;
else
img_done <= img_done;
end
always @(posedge video_clk or negedge rst_n) begin
if (img_done) begin
$stop; // 停止仿真
end
else if (video_de) begin // 写入数据
$fdisplay(output_file, "%h\t%h\t%h",
gamma_data_r,
gamma_data_g,
gamma_data_b); // 16进制写入
end
end
最后的有效信号、场信号和数据按上述代码修改即可。最后运行联合仿真,将生成的 txt 在 matlab 中恢复为图片显示。

4.3. 实验现象
连接好下载器,电源、HDMI_IN 口连接电脑、HDMD_OUT 口连接显示器,然后下载程序。


上图为测试原图像。

上图为经过 gamma 矫正后的图像,相对原图,经过 gamma 矫正后的图像细节更加清晰,读者可以通过修改 gamma 的值去改善图像增强的效果。