想要知道如何实现直方图均衡化,就必须先了解直方图这一概念,我们常说的直方图指的是灰度直方图,灰度直方图描述了一幅图像的灰度级统计信息,主要应用于图像分割、图像增强及图像灰度变换等处理过程。
从数学上来说,图像直方图描述的是图像各个灰度级的统计特性,它是用图像灰度值的一个函数来统计一幅图像中的各个灰度级出现的次数或概率。在实际应用中常常会用到归一化的直方图,假定一幅图像的像素总数为 N N N,灰度级总数为 L L L,其中灰度级为 g g g的像素总数为 N g N_g Ng。用总像素N除以各个灰度值出现的次数 N g N_g Ng,即可得到各个灰度级出现的概率,即
P g = N g ∑ N g P_g=\frac{N_g}{\sum N_g} Pg=∑NgNg
上式记为归一化的灰度直方图,也称为直方图概率密度函数,通常情况下记为PDF,不妨通过MATLAB编写代码来观察一下。
直方图均衡化又称为灰度均衡化,是指通过某种灰度映射使输入图像转换为在每一灰度级上都有近似相同的输出图像(即输出的直方图时均匀的)。在经过均衡化处理后的图像中,像素将占有尽可能多的灰度级并且分布均匀。因此这样的图像将具有较高的对比度和较大的动态范围。直方图均衡可以很好地解决相机过曝光或曝光不足的问题。对于离散的灰度级,相应的转换公式如下:
D B = f ( D A ) = D m a x A 0 ∗ ∑ i = 0 D A H ( i ) D_B=f(D_A)=\frac{D_{max}}{A_0}*\sum_{i=0}^{D_A}H(i) DB=f(DA)=A0Dmax∗i=0∑DAH(i)
上式中, D B D_B DB为转换后的灰度值, D A D_A DA为转换前的灰度值, D m a x D_{max} Dmax为最大灰度值(对于灰度图像就是255), A 0 A_0 A0为图像面积,即像素总数, H ( i ) H(i) H(i)为第 i i i级灰度级的像素个数。不妨通过MATLAB再次直观地展示:
以上工作对于FPGA来说是相当复杂的,需要考虑如下几点:
(1)统计工作至少要等到当前图像“”流过“之后才能完成。此限制了我们不可能对统计工作进行流水线统计与输出。
(2)必须对前期的统计结果进行缓存。
(3)在下一次统计前需要将缓存结果清零。
在直方图统计中,一般选择双口RAM作为缓存存储器,对于8位的深度图来说,统计结果的数据量并不大,因此选择片内存储。一方面统计模块需要与其他时序来配合,因此需要提供双边读写接口;另一方面,统计过程中需要地址信息,因此选择RAM形式的存储器。接下来就是确定双口RAM的参数,主要包括数据位宽及地址位宽。假定输入图像宽度为 I W IW IW,高度为 I H IH IH,数据位宽为 D W DW DW。那么待统计的像素总数为
P i x e l T o t a l = I W ∗ I H Pixel_{Total}=IW*IH PixelTotal=IW∗IH
像素灰度值的理论最大值为
P i x e l M a x = 2 D W − 1 Pixel_{Max}=2^{DW}-1 PixelMax=2DW−1
双口RAM的统计地址输入端为输入像素值,很明显,这个数据范围为 0 − 2 D W − 1 0-2^{DW}-1 0−2DW−1,因此,RAM的地址位宽最少为 D W DW DW。
双口RAM的数据输出端为输入统计值,很明显,这个数据范围在 0 − P i x e l T o t a l 0-Pixel_{Total} 0−PixelTotal,因此RAM的地址位宽最少为 l o g 2 ( P i x e l T o t a l ) log_2(Pixel_{Total}) log2(PixelTotal)。
本文是以320*240图像为例,在代码中调用了两个RAM模块,his_get模块用来存储直方图统计数据,gray_get模块则存放计算好的输入灰度级与均衡化之后的灰度级的映射关系。然后将输入灰度级作为gray_get模块的地址,输出的数据即为经过均衡化之后的图像。其中,his_get模块的配置如下所示:
gray_get模块的配置如下图所示:
对RAM的IP核感兴趣的同学可以去Xilinx官网搜索 P G 058 PG058 PG058文档进行了解。下面我们开始分析代码:
`timescale 1ns / 1ps
module histogram_equ(
input clk,
input rst_n,
input per_frame_vsync,
input per_frame_href,
input per_frame_clken,
input [7:0] per_img_8bit,
output post_frame_vsync,
output post_frame_href,
output post_frame_clken,
output [7:0]post_frame_8bit
);
parameter IMG_WIDTH = 9'd320 ; //图像宽度
parameter IMG_HEIGHT = 8'd240 ; //图像高度
parameter IMG_GRAY = 9'd256; //总共有256个灰度级
/**************************************************************直方图统计阶段****************************************************************/
//状态机
reg [3:0] state_1;
localparam IDLE = 4'b0001; //空闲状态
localparam CLEAR = 4'b0010; //清零状态,使用A端口写入0
localparam CUL = 4'b0100; //统计状态,使用B端口读出原有数据,与统计值相加后,从A端口写入
localparam GET = 4'b1000; //输出状态,使用B端口读出统计直方图数据
//ram的读写信号
reg his_wea,his_enb;
reg [7:0] his_addra,his_addrb;
reg [31:0] his_dina;
wire [31:0] his_doutb;
//清零阶段信号
reg per_frame_vsync_dly1;
wire clear_start;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
per_frame_vsync_dly1 <= 1'b0;
else
per_frame_vsync_dly1 <= per_frame_vsync;
end
assign clear_start = (per_frame_vsync == 1'b0)&&(per_frame_vsync_dly1 == 1'b1); //将帧信号下降沿作为清零阶段的起始标志位
reg [7:0] clear_cnt; //清零地址信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
clear_cnt <= 8'd0;
else if(state_1 == CLEAR)
clear_cnt <= clear_cnt + 8'd1;
else if(clear_cnt == IMG_GRAY - 1)
clear_cnt <= 8'd0;
else
clear_cnt <= 8'd0;
end
reg clear_flag; //清零使能信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
clear_flag <= 1'b0;
else if(clear_start == 1'b1)
clear_flag <= 1'b1;
else if(clear_cnt == IMG_GRAY - 1)
clear_flag <= 1'b0;
end
//统计阶段信号
//对输入的灰度值和数据有效信号延时1个时钟周期统计
reg [7:0] per_img_8bit_dly1;
reg [7:0] per_img_8bit_dly2;
reg per_frame_clken_dly1;
reg per_frame_clken_dly2;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
per_img_8bit_dly1 <= 8'd0;
per_img_8bit_dly2 <= 8'd0;
per_frame_clken_dly1<= 1'b0;
per_frame_clken_dly2<= 1'b0;
end
else begin
per_img_8bit_dly1 <= per_img_8bit;
per_img_8bit_dly2 <= per_img_8bit_dly1;
per_frame_clken_dly1<= per_frame_clken;
per_frame_clken_dly2<= per_frame_clken_dly1;
end
end
//相邻像素点灰度值相同的个数,从第2个像素点开始统计
reg [31:0] pix_same_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
pix_same_cnt <= 32'd1;
else if((per_frame_clken == 1'b1) && (per_frame_clken_dly1 == 1'b1)) begin
if(per_img_8bit == per_img_8bit_dly1)
pix_same_cnt <= pix_same_cnt + 32'd1;
else if(per_img_8bit != per_img_8bit_dly1)
pix_same_cnt <= 32'd1;
else
pix_same_cnt <= 32'd1;
end
else
pix_same_cnt <= 32'd1;
end
reg pix_same_flag;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
pix_same_flag <= 1'b0;
else if(per_frame_clken_dly2) begin
if(per_img_8bit != per_img_8bit_dly1)
pix_same_flag <= 1'b1;
else if((per_frame_clken == 1'b0) && (per_frame_clken_dly1 == 1'b1))
pix_same_flag <= 1'b1;
else
pix_same_flag <= 1'b0;
end
else
pix_same_flag <= 1'b0;
end
//将统计值与原本存储在RAM中的灰度值数据相加
reg [31:0] cul_data;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
cul_data <= 32'd0;
else
cul_data <= pix_same_cnt + his_doutb;
end
//对行数据进行计数,如果统计完320行数据,表明一帧图像数据结束
reg [7:0] row_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
row_cnt <= 8'd0;
else if(per_frame_vsync == 1'b1) begin
if((per_frame_clken == 1'b0) && (per_frame_clken_dly1 == 1'b1)) // 行数据信号下降沿
row_cnt <= row_cnt + 8'd1;
else if(row_cnt == IMG_HEIGHT)
row_cnt <= 8'd0;
end
else
row_cnt <= 8'd0;
end
//读出数据信号
reg [7:0] get_cnt; //读出地址信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
get_cnt <= 8'd0;
else if(state_1 == GET)
get_cnt <= get_cnt + 8'd1;
else if(get_cnt == IMG_GRAY - 1)
get_cnt <= 8'd0;
else
get_cnt <= 8'd0;
end
//状态机描述
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
state_1 <= IDLE;
else begin
case(state_1)
IDLE: begin
if(clear_start == 1'b1)
state_1 <= CLEAR;
else
state_1 <= IDLE;
end
CLEAR: begin
if(clear_cnt == IMG_GRAY - 1)
state_1 <= CUL;
else
state_1 <= CLEAR;
end
CUL: begin
if(row_cnt == IMG_HEIGHT)
state_1 <= GET;
else
state_1 <= CUL;
end
GET: begin
if(get_cnt == IMG_GRAY - 1)
state_1 <= IDLE;
else
state_1 <= GET;
end
default: state_1 <= IDLE;
endcase
end
end
his_get his_get_inst(
.clka(clk), // input wire clka
.wea(his_wea), // input wire [0 : 0] wea
.addra(his_addra), // input wire [7 : 0] addra
.dina(his_dina), // input wire [31 : 0] dina
.clkb(clk), // input wire clkb
.enb(his_enb), // input wire enb
.addrb(his_addrb), // input wire [7 : 0] addrb
.doutb(his_doutb) // output wire [31 : 0] doutb
);
//控制RAM的A端口(写端口)
always @(*) begin
if(state_1 == CLEAR) begin
his_wea = clear_flag;
his_addra = clear_cnt;
his_dina = 32'd0;
end
else if(state_1 == CUL) begin
his_wea = pix_same_flag;
his_addra = per_img_8bit_dly2;
his_dina = cul_data;
end
else begin
his_wea = 1'b0;
his_addra = 8'd0;
his_dina = 32'd0;
end
end
//控制RAM的B端口(读端口)
always @(*) begin
if(state_1 == CUL) begin
his_enb = per_frame_clken;
his_addrb = per_img_8bit;
end
else if(state_1 == GET) begin
his_enb = state_1 == GET;
his_addrb = get_cnt;
end
else begin
his_enb = 1'b0;
his_addrb = 8'd0;
end
end
/**************************************************************通过直方图数据计算对应的灰度级数据****************************************************************/
parameter IMG_TOTAL = 32'd76800; //图像总像素点个数:320*240=76800
reg [2:0] state_2;
localparam GRAY_IDLE = 3'b001; //空闲状态
localparam GRAY_CLEAR= 3'b010; //清空状态与写灰度级状态并存,前一个时钟周期清零地址,后一个时钟周期在对应地址写上灰度级
localparam GRAY_GET = 3'b100; //将原始图像灰度级数据作为地址,获得与之对应的灰度级数据,然后输出
reg gray_wea,gray_ena,gray_enb,gray_web;
reg [7:0] gray_addra,gray_addrb;
reg [31:0] gray_dina,gray_dinb;
wire [31:0] gray_douta,gray_doutb;
//GRAY_CLEAR阶段信号
reg [7:0] gray_clear_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
gray_clear_cnt <= 8'd0;
else if(state_2 == GRAY_CLEAR)
gray_clear_cnt <= gray_clear_cnt + 8'd1;
else if(gray_clear_cnt == IMG_GRAY - 1)
gray_clear_cnt <= 8'd0;
else
gray_clear_cnt <= 8'd0;
end
reg gray_clear_flag;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
gray_clear_flag <= 1'b0;
else if(row_cnt == IMG_HEIGHT)
gray_clear_flag <= 1'b1;
else if(gray_clear_cnt == IMG_GRAY - 1)
gray_clear_flag <= 1'b0;
end
wire write_flag;
assign write_flag = state_1 == GET;
reg [3:0] write_flag_dly1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
write_flag_dly1 <= 4'b0;
else
write_flag_dly1 <= {write_flag_dly1[2:0],write_flag};
end
reg [31:0] write_data1,write_data2,write_data3;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
write_data1 <= 32'd0;
else if(write_flag_dly1[0])
write_data1 <= write_data1 + his_doutb;
else
write_data1 <= 32'd0;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
write_data2 <= 32'd0;
write_data3 <= 32'd0;
end
else begin
write_data2 <= write_data1 * (IMG_GRAY - 1);
write_data3 <= write_data2/IMG_TOTAL;
end
end
reg [7:0] write_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
write_cnt <= 8'd0;
else if(write_flag_dly1[3])
write_cnt <= write_cnt + 8'd1;
else if(write_cnt == IMG_GRAY - 1)
write_cnt <= 8'd0;
else
write_cnt <= 8'd0;
end
//状态机编写
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
state_2 <= GRAY_IDLE;
else begin
case(state_2)
GRAY_IDLE: begin
if(row_cnt == IMG_HEIGHT)
state_2 <= GRAY_CLEAR;
else
state_2 <= GRAY_IDLE;
end
GRAY_CLEAR:begin
if(write_cnt == IMG_GRAY - 1)
state_2 <= GRAY_GET;
else
state_2 <= GRAY_CLEAR;
end
GRAY_GET:begin
if(row_cnt == IMG_HEIGHT)
state_2 <= GRAY_CLEAR;
else
state_2 <= GRAY_GET;
end
default:state_2 <= GRAY_IDLE;
endcase
end
end
gray_get gray_get_inst (
.clka(clk), // input wire clka
.ena(gray_ena), // input wire ena
.wea(gray_wea), // input wire [0 : 0] wea
.addra(gray_addra), // input wire [7 : 0] addra
.dina(gray_dina), // input wire [31 : 0] dina
.douta(gray_douta), // output wire [31 : 0] douta
.clkb(clk), // input wire clkb
.enb(gray_enb), // input wire enb
.web(gray_web), // input wire [0 : 0] web
.addrb(gray_addrb), // input wire [7 : 0] addrb
.dinb(gray_dinb), // input wire [31 : 0] dinb
.doutb(gray_doutb) // output wire [31 : 0] doutb
);
//控制A端口相关信号
always @(*) begin
if(state_2 == GRAY_CLEAR) begin
gray_ena = gray_clear_flag;
gray_wea = gray_clear_flag;
gray_addra = gray_clear_cnt;
gray_dina = 32'd0;
end
else if(state_2 == GRAY_GET) begin
gray_ena = per_frame_clken;
gray_wea = ~per_frame_clken;
gray_addra = per_img_8bit;
gray_dina = 32'd0;
end
else begin
gray_ena = 1'b0;
gray_wea = 1'b0;
gray_addra = 8'd0;
gray_dina = 32'd0;
end
end
//控制B端口相关信号
always @(*) begin
if(state_2 == GRAY_CLEAR) begin
gray_enb = write_flag_dly1[3];
gray_web = write_flag_dly1[3];
gray_addrb = write_cnt;
gray_dinb = write_data3;
end
else begin
gray_enb = 1'b0;
gray_web = 1'b0;
gray_addrb = 8'd0;
gray_dinb = 32'd0;
end
end
assign post_frame_vsync = per_frame_vsync_dly1;
assign post_frame_href = per_frame_clken_dly1;
assign post_frame_clken = per_frame_clken_dly1;
assign post_frame_8bit = gray_douta;
endmodule
根据功能需求,代码分为了两个部分,前一阶段统计灰度直方图数据,后一阶段根据公式计算得到映射关系,并存储起来。值得注意的是:统计的灰度直方图数据为前一帧图像,因此得到的映射关系是前一帧各灰度级与对应的输出灰度级的映射关系,但是我们将当前帧作为输入,利用前一帧的映射关系,虽然有差别,但差别不大,不影响结果。
直方图统计阶段,清零状态:当检测到帧同步信号下降沿时,开始清空RAM,方法很简单,往里面写0即可,当清空256个地址时,进入下一状态;统计状态:如果每来一个像素都对双口RAM进行一次寻址和写操作,显然降低了统计效率而提高了功耗,另外还有避免对同一地址进行读写操作所带来的冲突,采用一个相同灰度值计数器进行优化,即统计相邻像素点灰度级相同的个数,当灰度级出现不同时,再将计数器的值与读出的地址上的数据相加,然后写入,这样大大减少读写RAM的操作;输出状态:取出存储在RAM的直方图数据,给下一阶段使用。
直方图均衡化阶段:清零状态:在这一状态中,由于清空地址是计数器给的,从0-255,因此可以将其延迟一个时钟周期,作为双口RAM的写地址,将计算好的值写入RAM,不单独设置写数据状态,是要节约处理时间,不能等到下一帧数据到来时,RAM中所存储的映射关系还没准备好;输出状态:以下一帧数据的灰度值作为地址,读出RAM数据输出,即可得到均衡化之后的图像。
接下来我们来看看FPGA仿真效果如何:
Y分量直方图时序图:
左图为YCbCr色彩空间的Y分量(亮度信息)图像,右图为均衡化之后的图像,符合预期效果。
参考文献:牟新刚,周晓,郑晓亮.基于FPGA的数字图像处理原理及应用