FPGA图像处理_直方图均衡化(含源码)

本文详细介绍了如何在FPGA中通过双口RAM实现图像直方图统计和直方图均衡化的过程,包括清零、统计、映射计算等步骤,并展示了Y分量图像的处理前后效果。涉及的关键技术包括直方图、灰度级映射和双口RAM的使用。
摘要由CSDN通过智能技术生成

想要知道如何实现直方图均衡化,就必须先了解直方图这一概念,我们常说的直方图指的是灰度直方图,灰度直方图描述了一幅图像的灰度级统计信息,主要应用于图像分割、图像增强及图像灰度变换等处理过程。
从数学上来说,图像直方图描述的是图像各个灰度级的统计特性,它是用图像灰度值的一个函数来统计一幅图像中的各个灰度级出现的次数或概率。在实际应用中常常会用到归一化的直方图,假定一幅图像的像素总数为 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)=A0Dmaxi=0DAH(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=IWIH
像素灰度值的理论最大值为
P i x e l M a x = 2 D W − 1 Pixel_{Max}=2^{DW}-1 PixelMax=2DW1
双口RAM的统计地址输入端为输入像素值,很明显,这个数据范围为 0 − 2 D W − 1 0-2^{DW}-1 02DW1,因此,RAM的地址位宽最少为 D W DW DW
双口RAM的数据输出端为输入统计值,很明显,这个数据范围在 0 − P i x e l T o t a l 0-Pixel_{Total} 0PixelTotal,因此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的数字图像处理原理及应用

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值