FPGA实现图像的直方图均衡化
直方图均衡化又称为灰度均衡化,是指通过某种灰度映射使输入图像转换为在每一灰度级上都有近似相同的输出图像(即输出的直方图是均匀的)。在经过均衡化处理后的图像中,像素将占有尽可能多的灰度级并且分布均匀。因此,这样的图像将具有较高的对比度和较大的动态范围。直方图均衡可以很好地解决相机过曝光或曝光不足的问题。
一、MATLAB实现
%-------------------------------------------------------------------------- % 直方图均衡化 %-------------------------------------------------------------------------- close all clear all; clc; I = rgb2gray(imread('car.bmp')); Ieq=histeq(I); subplot(221),imshow(I);title('原图'); subplot(222),imhist(I); subplot(223),imshow(Ieq);title('直方图均衡化'); subplot(224),imhist(Ieq);
点击运行,得到如下结果:

从结果可以看出:图片对比度显著提高,直方图变得更均匀。
二、FPGA实现
1、理论分析
直方图均衡化的公式如下所示,H(i)为第 i 级灰度的像素个数,A0为图像的面积(即分辨率),Dmax为灰度最大值,即255。

2、实现步骤

和直方图拉伸的情况一样,直方图均衡化也分为真均衡化和伪均衡化。本次设计采用伪均衡化,即采用前一帧的图像进行统计、帧间隙进行累计和与归一化、当前帧做归一化后的映射输出。
统计工作至少要等到前一帧图像“流过”之后才能完成。此限制决定了我们难以在同一帧既统计又输出最终结果。必须对前期的统计结果进行缓存、累计和、归一化,这点是毋庸置疑的。在下一次统计前需要将缓存结果、累计和结果清零(图片则不需要清0),而归一化的结果则留着给当前帧输出使用。这里我考虑用 2 个 ram 来实现直方图均衡化的整个过程,以图片为例,用两帧图片的伪均衡化来实现。
整体构思如下所示:

完整代码如下:
//**************************************************************************
// *** 名称 : hist_equalization.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2020年11月
// *** 描述 : 直方图统计+直方图均衡+直方图映射
//**************************************************************************
module hist_equalization
//========================< 参数 >==========================================
#(
parameter H_DISP = 12'd480 , //图像宽度
parameter V_DISP = 12'd272 //图像高度
)
//========================< 端口 >==========================================
(
input wire clk , //时钟
input wire rst_n , //复位
//---------------------------------------------------
input wire Y_hsync , //Y分量行同步
input wire Y_vsync , //Y分量场同步
input wire [ 7:0] Y_data , //Y分量数据
input wire Y_de , //Y分量数据使能
//---------------------------------------------------
output reg hist_hsync , //hist行同步
output reg hist_vsync , //hist场同步
output wire [ 7:0] hist_data , //hist数据
output reg hist_de //hist数据使能
);
//========================< 信号 >==========================================
reg Y_vsync_r ;
reg Y_hsync_r ;
reg [ 7:0] Y_data_r ;
reg Y_de_r ;
wire hist_cnt_yes ;
wire hist_cnt_not ;
reg [31:0] hist_cnt ;
//ram1 ----------------------------------------------
wire wr_en_1 ;
wire [31:0] wr_data_1 ;
wire [ 7:0] wr_addr_1 ;
wire [ 7:0] rd_addr_1 ;
wire [31:0] rd_data_1 ;
//ram2 ----------------------------------------------
wire wr_en_2 ;
wire [31:0] wr_data_2 ;
wire [ 7:0] wr_addr_2 ;
wire [ 7:0] rd_addr_2 ;
wire [31:0] rd_data_2 ;
//ram2 ----------------------------------------------
wire Y_vsync_stop ;
reg [ 7:0] addr_cnt ;
reg [ 7:0] addr_cnt_r1 ;
reg [ 7:0] addr_cnt_r2 ;
reg [ 7:0] addr_cnt_r3 ;
reg [ 7:0] addr_cnt_r4 ;
reg addr_flag ;
reg addr_flag_r1 ;
reg addr_flag_r2 ;
reg addr_flag_r3 ;
reg addr_flag_r4 ;
reg [31:0] sum ;
//---------------------------------------------------
reg [31:0] step_1 ;
reg [ 7:0] step_2 ;
//==========================================================================
//== 打拍,后面用得到
//==========================================================================
always @(posedge clk) begin
Y_vsync_r <= Y_vsync;
Y_hsync_r <= Y_hsync;
Y_data_r <= Y_data;
Y_de_r <= Y_de;
end
//==========================================================================
//== 前一帧:直方图灰度统计
//==========================================================================
//数据前后拍进行比较
//---------------------------------------------------
assign hist_cnt_yes = Y_de_r && Y_data_r == Y_data; //相等,可以相加
assign hist_cnt_not = Y_de_r && Y_data_r != Y_data; //不等,只是一个
//灰度计数器
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
hist_cnt <= 32'b1;
end
else if(hist_cnt_not) begin
hist_cnt <= 32'b1;
end
else if(hist_cnt_yes) begin
hist_cnt <= hist_cnt + 1'b1;
end
else begin
hist_cnt <= 32'b0;
end
end
//统计结果输入到统计 ram1 中
//---------------------------------------------------
//前一帧按需要写入,帧间隙的ram1读出统计值后,下一拍对ram1清0
assign wr_en_1 = Y_vsync ? hist_cnt_not : addr_flag_r1;
//前一帧按需要写入,帧间隙的ram1读出统计值后,下一拍对ram1清0
assign wr_addr_1 = Y_vsync ? Y_data_r : addr_cnt_r1;
//前一帧按需要写入,帧间隙的ram1读出统计值后,下一拍对ram1清0
assign wr_data_1 = Y_vsync ? rd_data_1 + hist_cnt : 0;
//前一帧按像素地址输出,帧间隙按顺序输出统计结果,用于后面的计算
assign rd_addr_1 = Y_vsync ? Y_data : addr_cnt;
//双口ram,存储统计结果
//---------------------------------------------------
ram_32x256 u_ram_1
(
.clock (clk ),
.wren (wr_en_1 ),
.wraddress (wr_addr_1 ),
.data (wr_data_1 ),
.rdaddress (rd_addr_1 ),
.q (rd_data_1 )
);
//==========================================================================
//== 帧间隙,统计数据顺序输出,并进行累加和,耗费1clk
//==========================================================================
//计数256下,方便将ram1中结果按顺序读出
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
addr_cnt <= 8'b0;
end
else if(addr_flag) begin
addr_cnt <= addr_cnt + 1'b1;
end
else begin
addr_cnt <= 8'b0;
end
end
//帧结束标志
assign Y_vsync_stop = ~Y_vsync && Y_vsync_r;
//辅助计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
addr_flag <= 1'b0;
end
else if(Y_vsync_stop) begin //帧结束后拉高
addr_flag <= 1'b1;
end
else if(addr_cnt == 8'd255) begin //拉高256个高电平
addr_flag <= 1'b0;
end
end
//打拍,后面用得到
//---------------------------------------------------
always @(posedge clk) begin
addr_cnt_r1 <= addr_cnt;
addr_cnt_r2 <= addr_cnt_r1;
addr_cnt_r3 <= addr_cnt_r2;
addr_cnt_r4 <= addr_cnt_r3;
addr_flag_r1 <= addr_flag;
addr_flag_r2 <= addr_flag_r1;
addr_flag_r3 <= addr_flag_r2;
addr_flag_r4 <= addr_flag_r3;
end
//==========================================================================
//== 累加和,从开始到结果消耗2clk
//==========================================================================
//给出addr_cnt,过1拍才出rd_data_1,相当于消耗1clk,与之对齐的是addr_flag_r1
//累加和的计算又耗费1clk
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
sum <= 32'b0;
end
else if(addr_flag_r1) begin
sum <= sum + rd_data_1;
end
else begin
sum <= 32'b0;
end
end
//==========================================================================
//== 帧间隙,求和后进行均衡化运算:sum * 255 / (640*480)
//==========================================================================
//clk1,计算sum*255
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
step_1 <= 32'd0;
end
else if(addr_flag_r2) begin
step_1 <= sum * 255;
end
end
//clk2,计算除以分辨率
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
step_2 <= 8'd0;
end
else if(addr_flag_r3) begin
step_2 <= step_1 / (H_DISP*V_DISP);
end
end
//==========================================================================
//== 当前帧,直方图均衡化后的图像输出
//==========================================================================
//帧间隙写入
assign wr_en_2 = addr_flag_r4;
assign wr_addr_2 = addr_cnt_r4;
assign wr_data_2 = step_2;
//当前帧读出
assign rd_addr_2 = Y_data;
//双口ram,存储均衡结果,并在第二帧做映射输出
//---------------------------------------------------
ram_32x256 u_ram_2
(
.clock (clk ),
.wren (wr_en_2 ), //写使能
.wraddress (wr_addr_2 ), //顺序地址
.data (wr_data_2 ), //均衡化结果
.rdaddress (rd_addr_2 ),
.q (rd_data_2 )
);
//得到均衡化结果
assign hist_data = rd_data_2;
//ram读数据会落后读使能一拍,因此其他信号也要打拍对齐
//---------------------------------------------------
always @(posedge clk) begin
hist_vsync <= Y_vsync;
hist_hsync <= Y_hsync;
hist_de <= Y_de;
end
endmodule
接下来,我们看看代码里的步骤:
(1)前一帧:统计图像的直方图 H(i),统计结果实时输入到 ram1,注意输入数据要进行统计叠加,这算是一个难点;
//========================================================================== //== 前一帧:直方图灰度统计 //========================================================================== //数据前后拍进行比较 //--------------------------------------------------- assign hist_cnt_yes = gray_data_vld && gray_data_r == gray_data; //相等,可以相加 assign hist_cnt_not = gray_data_vld && gray_data_r != gray_data; //不等,只是一个 //灰度计数器 //--------------------------------------------------- always @(posedge clk or negedge rst_n) begin if(!rst_n) begin hist_cnt <= 32'b0; end else if(hist_cnt_not) begin hist_cnt <= 32'b1; end else if(hist_cnt_yes) begin hist_cnt <= hist_cnt + 1'b1; end else begin hist_cnt <= 32'b0; end end //统计结果输入到统计 ram1 中 //--------------------------------------------------- assign wr_en_1 = hist_cnt_not; assign wr_addr_1 = gray_data_r; assign wr_data_1 = rd_data_1 + hist_cnt; assign rd_addr_1 = gray_vsync ? addr_cnt : gray_data; //帧间隙按顺序输出,前一帧按像素地址输出 //双口ram,存储统计结果 //--------------------------------------------------- ram_32x256 u_ram_1 ( .clock (clk ), .wren (wr_en_1 ), .wraddress (wr_addr_1 ), .data (wr_data_1 ), .rdaddress (rd_addr_1 ), .q (rd_data_1 ) );
(2)前一帧到当前帧的间隙:设计计数器addr_cnt,计数0-255,以此为地址从 ram1 中输出统计结果,然后进行累加和
,注意一下时序对齐。
//给出addr_cnt,过1拍才出rd_data_1,相当于消耗1clk,与之对齐的是addr_flag_r1 //累加和的计算又耗费1clk //--------------------------------------------------- always @(posedge clk or negedge rst_n) begin if(!rst_n) begin sum <= 32'b0; end else if(addr_flag_r1) begin sum <= sum + rd_data_1; end else begin sum <= 32'b0; end end
(3)前一帧到当前帧的空隙:在进行第(2)步时,同时也要边进行均衡化,即
,结果输入到 ram2 中;这里公式计算要避免乘除法,需要一番技巧,注意一下时序对齐。
//========================================================================== //== 帧间隙,求和后进行均衡化运算 //== 图像分辨率640*480,为避免乘除法采用640*512来处理 //== [(2^5+2^4)+(2^2+2^1)] / 2^16, 为优化时序用流水线花2拍处理 //==========================================================================
(4)当前帧:均衡化后的数据输出,以像素为 ram2 地址,从 ram2 中实时输出均衡化后的映射结果,代替原像素输出,达到均衡化的目的。
//========================================================================== //== 当前帧,直方图均衡化后的映射输出 //========================================================================== ram_32x256 u_ram_2 ( .clock (clk ), .wren (addr_flag_r4 ), //写使能 .wraddress (addr_cnt_r4 ), //顺序地址 .data (step_2 ), //归一化结果 .rdaddress (gray_data ), .q (hist_data ) ); //ram读数据会落后读使能一拍,因此其他信号也要打拍对齐 //--------------------------------------------------- always @(posedge clk) begin hist_vsync <= gray_vsync; hist_hsync <= gray_hsync; hist_data_vld <= gray_data_vld; end
3、注意要点
(1)公式计算化简
以640x480为例,【255/(640x480)】的运算对于 FPGA 来说比较艰难,可以转换为移位,建议按 640x512 来算:

(2)ram
单口 ram 貌似不行,申请双口 ram IP 吧,一套接口就够了。
(3)时序对齐
ram发出读地址,数据要延迟一拍或两拍才出来(看IP设置)。累加和、公式计算等又消耗一定的拍数,设计过程中要时刻关注时序对齐,建议边仿真边设计。
三、结果展示
板子坏了,这次就用仿真做一下吧,采用 Matlab 将图片生成 pre_img.txt 文件,Verilog设计一个模仿 OV7725 的时序,图像数据部分读取该 pre_img.txt 文件。最后处理完的数据再写入到一个 post_img.txt文件,最后 Matlab 读取该 post_img.txt 文件,将结果还原为图片。全程只需要用到 Modelsim 和 Matlab 软件,比较方便。

后续:后来上版,发现效果远没有仿真那么好,可能哪里出了点问题......
参考资料:
1] OpenS Lee:FPGA开源工作室(公众号)
[2] 牟新刚、周晓、郑晓亮.基于FPGA的数字图像处理原理及应用[M]. 电子工业出版社,2017.

浙公网安备 33010602011771号