基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现
基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现
摘要:本文详细阐述了基于 FPGA 的 CLAHE(自适应限制对比度直方图均衡)算法的硬件verilog实现方案。CLAHE是一种强大的图像增强算法,广泛应用于医学影像、红外成像、低照度增强等领域。本文将从算法原理出发,深入讲解各模块的RTL架构设计,包括坐标计数器、直方图统计、CDF计算、双线性插值映射以及乒乓RAM管理等核心模块的实现细节。
项目开源地址:https://github.com/Passionate0424/CLAHE_verilog
开源不易,辛苦各位看官点点star!!
一、CLAHE算法基本原理
1.1 算法背景
CLAHE(Contrast Limited Adaptive Histogram Equalization,对比度受限的自适应直方图均衡)是对传统自适应直方图均衡(AHE)的改进。AHE通过将图像划分为多个子区域(称为 “Tiles”),对每个Tile独立进行直方图均衡化,从而适应图像的局部特性。然而,AHE在噪声较大的平坦区域(如天空、墙面)容易过度放大噪声,产生伪影。
CLAHE通过引入对比度限制机制来解决此问题。
1.2 核心处理步骤
1.2.1 图像分块 (Tiling)
将整幅图像划分为 M × N M \times N M×N 个连续且不重叠的矩形子区域(Tiles)。本设计采用 4×4=16 分块。
1.2.2 直方图计算 (Histogram Calculation)
为每个Tile独立计算其灰度直方图 H ( i ) H(i) H(i),其中 i i i 是灰度级(0-255)。
1.2.3 对比度限制 (Contrast Limiting / Clipping)
这是CLAHE的关键步骤。首先设定"裁剪阈值"(Clip Limit),根据归一化的裁剪因子 β \beta β、Tile总像素数 N t i l e N_{tile} Ntile 和灰度级数 L L L 计算:
T c l i p = β × N t i l e L T_{clip} = \beta \times \frac{N_{tile}}{L} Tclip=β×LNtile
遍历Tile直方图,将超出阈值的像素数裁剪:
H c l i p p e d ( i ) = { T c l i p if H ( i ) > T c l i p H ( i ) if H ( i ) ≤ T c l i p H_{clipped}(i) = \begin{cases} T_{clip} & \text{if } H(i) > T_{clip} \\ H(i) & \text{if } H(i) \le T_{clip} \end{cases} Hclipped(i)={TclipH(i)if H(i)>Tclipif H(i)≤Tclip
1.2.4 溢出重分配 (Redistribution)
将所有灰度级裁剪下来的像素总数(溢出量)均匀重分配到所有灰度级中:
N o v e r f l o w = ∑ i = 0 L − 1 max ( 0 , H ( i ) − T c l i p ) N_{overflow} = \sum_{i=0}^{L-1} \max(0, H(i) - T_{clip}) Noverflow=i=0∑L−1max(0,H(i)−Tclip)
H f i n a l ( i ) = H c l i p p e d ( i ) + N o v e r f l o w L H_{final}(i) = H_{clipped}(i) + \frac{N_{overflow}}{L} Hfinal(i)=Hclipped(i)+LNoverflow
硬件实现优化:由于RTL使用整数统计直方图,同时需要保证直方图总和不变,这里我们采用整除+余数分配策略:
a v g = ⌊ N o v e r f l o w / L ⌋ , r e m a i n d e r = N o v e r f l o w m o d L avg = \lfloor N_{overflow} / L \rfloor, \quad remainder = N_{overflow} \mod L avg=⌊Noverflow/L⌋,remainder=NoverflowmodL
H f i n a l ( i ) = { H c l i p p e d ( i ) + a v g + 1 if i < r e m a i n d e r H c l i p p e d ( i ) + a v g if i ≥ r e m a i n d e r H_{final}(i) = \begin{cases} H_{clipped}(i) + avg + 1 & \text{if } i < remainder \\ H_{clipped}(i) + avg & \text{if } i \ge remainder \end{cases} Hfinal(i)={Hclipped(i)+avg+1Hclipped(i)+avgif i<remainderif i≥remainder
1.2.5 生成映射函数 (Mapping Function)
对于每个块(tile),基于处理后的直方图计算累积分布函数(CDF),归一化后作为映射查找表,即输入像素h灰度值为j,映射后输出灰度值为 L U T ( j ) LUT(j) LUT(j)
C D F ( j ) = ∑ i = 0 j H f i n a l ( i ) CDF(j) = \sum_{i=0}^{j} H_{final}(i) CDF(j)=i=0∑jHfinal(i)
L U T ( j ) = L − 1 N t i l e × C D F ( j ) LUT(j) = \frac{L-1}{N_{tile}} \times CDF(j) LUT(j)=NtileL−1×CDF(j)
1.2.6 双线性插值 (Bilinear Interpolation)
为消除Tile边界的"块效应",每个像素的输出值通过查找周围四个Tile中心的LUT映射值 V V V,再进行双线性插值加权平均得出:
V t o p = ( 1 − Δ x ) ⋅ V T L + Δ x ⋅ V T R V_{top} = (1 - \Delta x) \cdot V_{TL} + \Delta x \cdot V_{TR} Vtop=(1−Δx)⋅VTL+Δx⋅VTR
V b o t t o m = ( 1 − Δ x ) ⋅ V B L + Δ x ⋅ V B R V_{bottom} = (1 - \Delta x) \cdot V_{BL} + \Delta x \cdot V_{BR} Vbottom=(1−Δx)⋅VBL+Δx⋅VBR
P o u t = ( 1 − Δ y ) ⋅ V t o p + Δ y ⋅ V b o t t o m P_{out} = (1 - \Delta y) \cdot V_{top} + \Delta y \cdot V_{bottom} Pout=(1−Δy)⋅Vtop+Δy⋅Vbottom
二、硬件架构设计
2.1 顶层模块架构
顶层模块 clahe_top 负责整个CLAHE系统的集成和协调,管理各子模块间的数据流和控制流。主要包含以下子模块:
| 模块名称 | 功能描述 |
|---|---|
clahe_coord_counter | 坐标计数与Tile定位 |
clahe_histogram_stat | 直方图实时统计 |
clahe_clipper_cdf | 对比度限制与CDF计算 |
clahe_mapping_parallel | 双线性插值映射输出 |
clahe_ram_16tiles_parallel | 32块RAM乒乓管理 |
因为所有tile的直方图统计在一帧输入结束后才统计完成,所以我们在帧间隙进行逐个tile的CDF计算和LUT生成。使用乒乓操作,一组ram用于统计当前输入帧的直方图数据,一组ram保存上一帧帧间隙中计算得到的查找表,帧开始的vsync上升沿二者切换,实现对视频输入的实时处理。
乒乓控制逻辑:在CDF计算完成时切换 ping_pong_flag,充分利用帧间隙时间,确保下一帧VSYNC上升沿来临前,乒乓切换已完成:
// 乒乓切换:在CDF完成时切换 always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin ping_pong_flag <= 1'b0; end else if (cdf_done_posedge) begin // 优化:在CDF完成时立即切换ping_pong // 此时CDF LUT已经完全写入RAM,可以安全切换 ping_pong_flag <= !ping_pong_flag; end end 2.2 坐标计数器模块 (clahe_coord_counter)
该模块实时计算输入像素的全局坐标、所属Tile索引和Tile内相对坐标,为直方图统计和像素映射提供位置信息。
设计要点:
- 在
href有效期间递增横向坐标x_cnt,行结束时递增纵向坐标y_cnt - 使用比较器链代替除法器计算Tile索引(节省资源)
- 块内坐标使用移位加法计算,减少资源使用
Tile索引计算原理:
// 横向tile索引计算(x_cnt除以320) // 通过比较x_cnt的范围来确定tile_x的值 always @(*) begin if (x_cnt < 320) // 0-319像素 -> tile 0 tile_x = 2'd0; else if (x_cnt < 640) // 320-639像素 -> tile 1 tile_x = 2'd1; else if (x_cnt < 960) // 640-959像素 -> tile 2 tile_x = 2'd2; else // 960-1279像素 -> tile 3 tile_x = 2'd3; end // tile总索引:使用位拼接 {tile_y, tile_x} 等价于 tile_y*4 + tile_x tile_idx = {tile_y, tile_x}; // 4位tile索引,范围0-15 块内坐标优化计算(使用移位替代乘法):
// 横向偏移量计算:tile_x * 320 = tile_x * (256 + 64) // = (tile_x << 8) + (tile_x << 6) wire [10:0] tile_x_offset; assign tile_x_offset = ({tile_x, 8'd0}) + ({tile_x, 6'd0}); // 纵向偏移量计算:tile_y * 180 = tile_y * (128 + 32 + 16 + 4) // = (tile_y << 7) + (tile_y << 5) + (tile_y << 4) + (tile_y << 2) wire [9:0] tile_y_offset; assign tile_y_offset = ({tile_y, 7'd0}) + ({tile_y, 5'd0}) + ({tile_y, 4'd0}) + ({tile_y, 2'd0}); // 相对坐标 = 全局坐标 - 偏移量 assign local_x = x_cnt[8:0] - tile_x_offset[8:0]; assign local_y = y_cnt[7:0] - tile_y_offset[7:0]; 2.3 直方图统计模块 (clahe_histogram_stat)
该模块对每个Tile的256个灰度级进行实时统计,使用3级流水线实现读-增-写操作。由于没有两个端口同时分别进行读写的需求,这里我们使用伪双端口RAM即可,节约资源,后续RAM控制模块会具体讲到。
流水线结构:
- Stage 1:输入打拍 + 相邻相同检测
- Stage 2:RAM读取 + 旁路数据选择
- Stage 3:RAM写入
2.3.1 读写冲突问题分析
对于流水读写问题,需考虑流水线深度内的数据冲突问题。也就是体系结构中的数据冒险。
对于流水读写RAM的情况,极易出现下列情况:
冲突1:连续相同像素值
例如像素序列:100, 100, 50,对于第二个100像素,读取统计旧值时,第一个100的累加值尚未写入,导致第二个像素累加值错误。
冲突2:间隔相同像素值(流水线深度冲突)
例如像素序列:100, 50, 100...(间隔2周期,< 流水线深度3),第二个100读取时,第一个100正在写入,发生读写冲突。双端口RAM在发生读写冲突时存在读数据不可靠的问题(且部分厂家的伪双端口RRAM不能配置为写优先或者读优先,实际读取值很可能是x不定态),需要进行处理。
2.3.2 冲突解决方案
问题1解决方案:检测连续输入的相同像素值,由于后面的像素读取统计值相当于比实际少了1,我们可以在写入时+2弥补。
// Stage 1: 相邻相同检测 always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin same_as_prev <= 1'b0; end else begin // 检测相邻相同:当前输入与上一周期输入比较 if ((in_href && in_vsync && clear_done) && valid_s1 && (in_y == pixel_s1) && (tile_idx == tile_s1)) begin same_as_prev <= 1'b1; end else begin same_as_prev <= 1'b0; end end end // Stage 2: 设置增量:相邻相同+2,否则+1 if (same_as_prev) begin increment_s2 <= 2'd2; end else begin increment_s2 <= 2'd1; end 问题2解决方案:使用旁路逻辑解决读写冲突。若当前周期发生写地址与读地址相同,寄存当前写数据作为读取值(相当于强制实现写优先,避免综合后行为和使用的RAM行为模型不一致的问题):
// 冲突检测:Stage1读地址 == Stage3写地址 wire conflict = (pixel_s1 == pixel_s3) && (tile_s1 == tile_s3) && valid_s3; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin bypass_valid <= 1'b0; bypass_data <= 16'd0; end else begin if (conflict) begin bypass_valid <= 1'b1; bypass_data <= ram_wr_data_s3; // 保存写入的数据 end else begin bypass_valid <= 1'b0; end end end // 数据选择:旁路优先 wire [15:0] selected_data = bypass_valid ? bypass_data : ram_rd_data_b; 通过以上两种方法结合,连续三个周期输入像素的情况也可以正确处理(当前输入像素的读取值用正在写入的数据替代,在此基础上+2写入,累积写入值正确)。本方法相当于对写回的统计值进行补偿修正,保证写入的统计值完全正确,可以完美解决数据冒险的问题。统计结果没有任何误差。
2.4 对比度限制与CDF计算模块 (clahe_clipper_cdf)
该模块在histogram结束后(帧间隙期间),对每帧图像16个Tile的直方图数据进行Clip阈值限制裁剪和CDF计算,最后归一化生成像素映射查找表。
有限状态机流程:
| 状态 | 周期数 | 说明 |
|---|---|---|
| READ_HIST_CLIP | 257 | 读取直方图 + 裁剪 |
| CLIP_REDIST | 257 | 仅在有溢出时执行,重分配溢出值 |
| CALC_CDF | 257 | 累积分布函数计算 |
| WRITE_LUT | 259 | 3级流水线归一化写入 |
| NEXT_TILE | 1 | Tile切换 |
| DONE | 1 | 产生cdf_done脉冲 |
TODO:写到这里发觉CLIP_REDIST应该可以和CALC_CDF阶段合并,进一步节约时间提高帧率,后续有时间优化一下(也欢迎各位同仁向仓库贡献代码)
时序分析:
- 每块Tile总周期数:约 257 + 257 + 257 + 259 + 1 + 1 = 1032 257+257+257+259+1+1=1032 257+257+257+259+1+1=1032 周期
- 16块耗时: 16 × 1032 = 16512 16 \times 1032 = 16512 16×1032=16512 周期
- 在96MHz时钟频率下耗时约172μs
- 1280×720@30fps帧间隙约33ms,CDF模块处理时间充足
归一化公式(标准CLAHE实现):
L U T ( j ) = ( C D F ( j ) − C D F m i n ) × 255 C D F m a x − C D F m i n LUT(j) = \frac{(CDF(j) - CDF_{min}) \times 255}{CDF_{max} - CDF_{min}} LUT(j)=CDFmax−CDFmin(CDF(j)−CDFmin)×255
2.5 RAM管理模块 (clahe_ram_16tiles_parallel)
该模块负责管理32块伪双端口RAM,实现乒乓操作、四块并行读取和多端口仲裁。帧内RAM内的数据作为直方图统计值,帧间隙计算映射值写回该组RAM,下一帧作为映射LUT使用。像素灰度值直接作为读写地址,所以RAM深度为256。
乒乓双组RAM架构:
| 帧状态 | RAM_A组用途 | RAM_B组用途 |
|---|---|---|
| 帧N (ping_pong_flag=0) | 统计(Port A写,Port B读) | 映射(Port B四块并行只读) |
| 帧N+1 (ping_pong_flag=1) | 映射(Port B四块并行只读) | 统计(Port A写,Port B读) |
并行读取接口设计:
由于mapping模块中的双线性插值需要读取当前像素最近的四个块(Tile)的输出LUT,为实现全流水,设计了四块并行读取功能:
三、仿真验证
鉴于图像区域每个分块都需要分配一块伪双端口BRAM,为减少资源占用,Baseline工程采用 4 × 4 = 16 4 \times 4 = 16 4×4=16 分块设计。虽然实际输出效果远不如 8 × 8 8 \times 8 8×8 Tile版本,但效果优于传统的HE算法。
ModelSim仿真结果:
四、优化方向展望
基础实现版本在面对高分辨率(HD/FHD)和更精细的分块(64-tile)需求时,存在以下挑战:
- 时序瓶颈:组合逻辑过深,关键路径延迟达35ns+,频率上不去(仅~28MHz)
- 资源消耗大:直接扩展到64-tile将消耗大量BRAM资源
- RAM利用率低:每块RAM实际容量远小于单block BRAM容量,存在浪费
针对这些问题,可以应用VLSI DSP信号处理理论中的核心优化技术进行改进:
| 优化技术 | 应用目标 |
|---|---|
| 割集流水线 (Cut-Set Pipelining) | 切断CDF计算中的长组合逻辑路径 |
| 重定时 (Retiming) | 解决深度流水线引入的控制与数据路径对齐问题 |
| 算法强度缩减 (Strength Reduction) | 优化插值运算,减少乘法器使用 |
| 硬件折叠 (Folding) | 巧妙设计地址映射实现ram复用 |
通过系统性地应用这些技术,可以大幅提升工作频率并显著降低资源消耗,实现真正的高性能实时视频图像增强方案。
通过系统性地应用这些技术,优化版本取得了显著的性能提升。以下是64-Tile版本的基础版本与优化版本在Xilinx 7系列FPGA上的对比数据:
资源消耗对比:
| 资源类型 | Baseline (64t) | Optimized (64t) | 变化幅度 |
|---|---|---|---|
| LUTs (逻辑单元) | 8,014 | 3,738 | ↓ 53.4% |
| Registers (寄存器) | 637 | 3,281 | ↑ 415% |
| Block RAM (Tiles) | 66 | 18 | ↓ 72.7% |
| F7/F8 Muxes | 1,024 | 52 | ↓ 95.0% |
寄存器数量增加是流水线技术"用面积换速度"的体现,符合预期的设计权衡。
时序性能对比:
| 指标 | Baseline @ 74MHz | Optimized @ 100MHz |
|---|---|---|
| WNS (最差负裕量) | -22.347 ns (Failed) | +4.704 ns (Met) |
| 理论最高频率 (Fmax) | ~28 MHz | ~188 MHz |
| 关键路径延迟 | 35.5 ns | 5.30 ns |
| 逻辑级数 | 185 级 | 6 级 |
优化后的设计不仅各项时序指标完全满足 1280×720 甚至更高分辨率的实时处理需求,而且在资源效率上达到了极优水平。
注:优化版本的详细实现可联系作者获取 [email protected]。
五、总结
本文详细介绍了CLAHE算法在FPGA上的硬件实现方案,包括:
- 算法原理:分块、直方图统计、对比度限制、溢出重分配、CDF计算、双线性插值
- 模块化架构:坐标计数器、直方图统计、CDF计算、映射输出、RAM管理
- 关键设计技巧:
- 比较器链替代除法器计算Tile索引
- 移位加法替代乘法计算偏移量
- 3级流水线处理直方图统计的读写冲突
- 乒乓RAM架构实现帧级并行处理
- 四块并行读取支持全流水双线性插值
该设计实现了1280×720@30fps的实时处理能力,验证了CLAHE算法硬件化的可行性。
参考资料:
- K. K. Parhi, VLSI Digital Signal Processing Systems: Design and Implementation
- Karel Zuiderveld, Contrast Limited Adaptive Histogram Equalization (Graphics Gems IV)
作者:Passionate.Z
项目地址:https://github.com/Passionate0424/CLAHE_verilog
如有问题欢迎交流讨论!