其实网上有不少的直方图均衡的文章,我这原理就不详细说明了,有想了解原理的小伙伴可以看这一篇https://blog.csdn.net/qq_31347869/article/details/89395485。我在此主要讲的是直方图均衡的FPGA实现
首先讲一下大体的思路:
为了方便以分辨率为128*128,256灰度值的bmp图像为例,取原始图片的像素点灰度值为nk。
整个直方图均衡过程可以表示为这个公式,可按照不同的图片分辨率和灰度大小进行调换 :
其中的Fk是我们最后的直方图均衡的结果
最后我们新图像M(i,j)这一点的灰度值为:F(n(i,j))
FPGA具体处理流程
话不多说,先上结果展示,由于得出的图像通过VGA输出到显示器显示,再拍照上传,所以不太清晰,但是还是可以看出效果很不错。
一.得到不同灰度值的像素点个数
由于图片的像素点的灰度信息是储存在rom中,一上面所设图片为例,rom中便有128*128个点,但只有0-255共256种的不同灰度值,所以我们需要的第一步工作就是采集到256种不同灰度值的像素个数且进行储存。
在这里我用到了ram进行储存像素个数,将rom输出的灰度值直接连到ram的地址线上,有一个这样的灰度值输出就在ram的相应地址进行加1储存,这样便按照不同灰度值记录下了像素点个数。
//由于采用的简单ram,不能同时进行读写,所以在进行灰度值像素计数时需要先进行读出像素点个数再加1进行储存,
//耗时为两个时钟,故rom读出时钟减慢
reg clk_r;
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_r <= 0;
else if(~add_done)
clk_r <= ~clk_r;
end
wire [13:0] rom_addr = add_done ? addr : cnt_addr;
//addr为最后lcd所输入的地址,cnt_addr为0~128*128的像素值采集
rom u_rom
(
.address (rom_addr),
.clock (add_done ? clk : clk_r),
.q (image_data)
);
// 直方图均衡中的加操作
reg [7:0] ram_addr_read_add;
reg [7:0] ram_addr_write_add;
wire [8:0] ram_data_read_add;
reg [8:0] ram_data_write_add;
// 直方图均衡计数
wire [7:0] ram_addr_read = cnt_flag ? image_data : ram_addr_read_add;
//cnt_flag为1时均衡化结束,通过F(i,j) = F(n(i,j)),把image_data输入到ram得到处理后灰度值
reg [7:0] ram_addr_write;
wire [8:0] ram_data_read;
wire [8:0] ram_data_write = ram_data_read >= 9'd511 ? 9'd511 : ram_data_read+ 1;
//储存ram
ram ram_inst
(
.aclr ( ~rst_n ),
.clock ( clk ),
.data ( ram_data_write),
.rdaddress ( ram_addr_read ),
.rden ( 1'b1 ),
.wraddress ( ram_addr_write ),
.wren ( cnt_flag ),
.q ( ram_data_read )
);
//像素点个数计数,0~128*128
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt_addr <= 0;
cnt_flag <= 1;
end
else
begin
cnt_addr <= (cnt_addr < 15'd16383 - 1) ? cnt_addr + 1 : 15'd16383 - 1;
cnt_flag <= (cnt_addr < 15'd16383 - 1) ? 1 : 0;
end
end
//由于在读地址写入后,读数据需要一个周期,所以把写地址进行了一个周期的延时以达到写数据和地址对应
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
ram_addr_write <= 0;
else
ram_addr_write <= ram_addr_read;
end
二.获得不同灰度累加后的值
其实简单的说对不同灰度点的值进行累加处理没有这方面基础的人确实是不太理解,所以还是先搞清楚原理再看。
不同灰度值累加涉及到两步①将现在ram种灰度值的像素点读取出来②进行累加处理后将结果储存到另一个ram_add中。其实这两步看起来都很简单,但是实际放到fpga的电路其中不太容易。
由于已知有256个灰度,所以设定ram_addr_write_add为0~255,在小于255的过程中写进ram_add的数据ram_data_write_add <= (ram_data_read + add_reg) >> 6,这里把*255/(128*128)做了一个整体运算相当于最后右移6位。ram_data_read为第一个ram中所得到的值,add_reg为累加的寄存值。
reg add_done; //处理完成标志位,置1后开始lcd的读写
reg [15:0] add_reg; //累加寄存器
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
ram_addr_write_add <= 0;
ram_data_write_add <= 0;
ram_addr_read_add <= 0;
add_reg <= 0;
add_done <= 0;
end
else if(~cnt_flag && ram_addr_write_add <= 8'd254)
begin
ram_addr_read_add <= ram_addr_read_add + 1;
ram_addr_write_add <= ram_addr_write_add + 1;
add_reg <= add_reg + ram_data_read;
ram_data_write_add <= (ram_data_read + add_reg) >> 6;
end
else
begin
ram_addr_read_add <= ram_addr_read_add;
ram_addr_write_add <= ram_addr_write_add;
ram_data_write_add <= ram_data_write_add;
add_done <= ram_addr_write_add == 255 ? 1 : 0;
end
end
三.将新的灰度值赋给图像像素点
将新的灰度值给图像像素的方法前面提到,以坐标(i,j)为例,新像素M(i,j) = F(n(i,j))
故需要知道此像素点(i,j)初始的灰度值,这里还是由rom给到,把rom中得到的灰度值n(i,j)作为地址输出到ram_add中,此时得到的值便是均衡化后的灰度值M(i,j),把M(i,j)输出到想应像素点即完成。
完成均衡化代码如下,此时的addr地址为lcd给入,相当于可从0-128*128.
module eq_image
(
input clk,
input rst_n,
input [13:0] addr,
output eq_done,
output [7:0] result
);
assign eq_done = add_done;
wire [7:0] image_data;
reg cnt_flag;
reg [13:0] cnt_addr;
//由于采用的简单ram,不能同时进行读写,所以在进行灰度值像素计数时需要先进行读出像素点个数再加1进行储存,
//耗时为两个时钟,故rom读出时钟减慢
reg clk_r;
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_r <= 0;
else if(~add_done)
clk_r <= ~clk_r;
end
wire [13:0] rom_addr = add_done ? addr : cnt_addr;
//addr为最后lcd所输入的地址,cnt_addr为0~128*128的像素值采集
rom u_rom
(
.address (rom_addr),
.clock (add_done ? clk : clk_r),
.q (image_data)
);
// 直方图均衡中的加操作
reg [7:0] ram_addr_read_add;
reg [7:0] ram_addr_write_add;
wire [8:0] ram_data_read_add;
reg [8:0] ram_data_write_add;
// 直方图均衡计数
wire [7:0] ram_addr_read = cnt_flag ? image_data : ram_addr_read_add;
//cnt_flag为1时均衡化结束,通过F(i,j) = F(n(i,j)),把image_data输入到ram得到处理后灰度值
reg [7:0] ram_addr_write;
wire [8:0] ram_data_read;
wire [8:0] ram_data_write = ram_data_read >= 9'd511 ? 9'd511 : ram_data_read+ 1;
ram ram_inst
(
.aclr ( ~rst_n ),
.clock ( clk ),
.data ( ram_data_write),
.rdaddress ( ram_addr_read ),
.rden ( 1'b1 ),
.wraddress ( ram_addr_write ),
.wren ( cnt_flag ),
.q ( ram_data_read )
);
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt_addr <= 0;
cnt_flag <= 1;
end
else
begin
cnt_addr <= (cnt_addr < 15'd16383 - 1) ? cnt_addr + 1 : 15'd16383 - 1;
cnt_flag <= (cnt_addr < 15'd16383 - 1) ? 1 : 0;
end
end
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
ram_addr_write <= 0;
else
ram_addr_write <= ram_addr_read;
end
ram ram_inst_add
(
.aclr ( ~rst_n),//低位有效,故以此作为复位信号
.clock ( clk ),
.data ( ram_data_write_add),
.rdaddress ( image_data ),
.rden ( add_done ),
.wraddress ( ram_addr_write_add ),
.wren ( ~cnt_flag && ~add_done),
.q ( result )
);
reg add_done;
reg [15:0] add_reg;
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
ram_addr_write_add <= 0;
ram_data_write_add <= 0;
ram_addr_read_add <= 0;
add_reg <= 0;
add_done <= 0;
end
else if(~cnt_flag && ram_addr_write_add <= 8'd254)
begin
ram_addr_read_add <= ram_addr_read_add + 1;
ram_addr_write_add <= ram_addr_write_add + 1;
add_reg <= add_reg + ram_data_read;
ram_data_write_add <= (ram_data_read + add_reg) >> 6;
end
else
begin
ram_addr_read_add <= ram_addr_read_add;
ram_addr_write_add <= ram_addr_write_add;
ram_data_write_add <= ram_data_write_add;
add_done <= ram_addr_write_add == 255 ? 1 : 0;
end
end
endmodule