基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现

基于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)={Tclip​H(i)​if H(i)>Tclip​if 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−1​max(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=Noverflow​modL

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)+avg​if 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∑j​Hfinal​(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)=Ntile​L−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_parallel32块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_CLIP257读取直方图 + 裁剪
CLIP_REDIST257仅在有溢出时执行,重分配溢出值
CALC_CDF257累积分布函数计算
WRITE_LUT2593级流水线归一化写入
NEXT_TILE1Tile切换
DONE1产生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)需求时,存在以下挑战:

  1. 时序瓶颈:组合逻辑过深,关键路径延迟达35ns+,频率上不去(仅~28MHz)
  2. 资源消耗大:直接扩展到64-tile将消耗大量BRAM资源
  3. 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,0143,738↓ 53.4%
Registers (寄存器)6373,281↑ 415%
Block RAM (Tiles)6618↓ 72.7%
F7/F8 Muxes1,02452↓ 95.0%
寄存器数量增加是流水线技术"用面积换速度"的体现,符合预期的设计权衡。

时序性能对比

指标Baseline @ 74MHzOptimized @ 100MHz
WNS (最差负裕量)-22.347 ns (Failed)+4.704 ns (Met)
理论最高频率 (Fmax)~28 MHz~188 MHz
关键路径延迟35.5 ns5.30 ns
逻辑级数185 级6 级

优化后的设计不仅各项时序指标完全满足 1280×720 甚至更高分辨率的实时处理需求,而且在资源效率上达到了极优水平。

:优化版本的详细实现可联系作者获取 [email protected]

五、总结

本文详细介绍了CLAHE算法在FPGA上的硬件实现方案,包括:

  1. 算法原理:分块、直方图统计、对比度限制、溢出重分配、CDF计算、双线性插值
  2. 模块化架构:坐标计数器、直方图统计、CDF计算、映射输出、RAM管理
  3. 关键设计技巧
    • 比较器链替代除法器计算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

如有问题欢迎交流讨论!

Read more

HDFS数据块机制深度解析:块大小设计与存储哲学

HDFS数据块机制深度解析:块大小设计与存储哲学

HDFS数据块机制深度解析:块大小设计与存储哲学 * 引言:块——HDFS存储的核心抽象 * 一、HDFS默认块大小 * 1.1 版本演进与默认值 * 1.2 查看和验证块大小 * 1.3 配置文件中的设置 * 二、为什么HDFS采用块存储? * 2.1 核心设计思想 * 2.2 详细解析:为什么块存储如此重要? * **2.2.1 减少寻址开销,提升I/O效率** * **2.2.2 支持超大文件,超越单机限制** * **2.2.3 简化存储设计,降低元数据复杂度** * **2.2.4 便于数据复制,增强容错性** * **2.2.5 支持数据本地性,

By Ne0inhk
【Linux系统】解明进程优先级与切换调度O(1)算法

【Linux系统】解明进程优先级与切换调度O(1)算法

各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页:落羽的落羽 文章目录 * 一、进程优先级的概念 * 二、查看优先级信息 * 1. PRI 与 NI 的理解 * 2. 修改nice值 * 三、进程调度切换 * 1. list_head 与 prio_array 结构 * 2. 活跃140队列与过期140队列 * 四、补充概念:竞争、独立、并行、并发 一、进程优先级的概念 CPU的资源是有限的,所以CPU的运行队列中的所有进程是不可能同时得到资源的。这就是为什么运行队列是一个“队列”,而CPU分配资源的先后顺序,就是指进程的优先级。 二、查看优先级信息 使用ps -l命令,可以查看系统中更详细的进程信息:

By Ne0inhk
Flutter 三方库 double_linked_list 本地化双向链表引擎鸿蒙核心侧适配深探:极尽榨干链表重构算力上限与迭代吞吐性能以支撑复杂游戏数据游标-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 double_linked_list 本地化双向链表引擎鸿蒙核心侧适配深探:极尽榨干链表重构算力上限与迭代吞吐性能以支撑复杂游戏数据游标-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 double_linked_list 本地化双向链表引擎鸿蒙核心侧适配深探:极尽榨干链表重构算力上限与迭代吞吐性能以支撑复杂游戏数据游标推演机制 在开发鸿蒙平台的高性能应用(如实时数据流处理、复杂的 UI 撤销/重做逻辑或底层缓存系统)时,如何实现极速的线性数据操作?double_linked_list 库提供了一个纯粹、高效的双向链表实现。本文将详解该库在 OpenHarmony 上的适配要点。 前言 什么是 double_linked_list?不同于标准的 List(基于数组,随机插入代价大),双向链表通过节点间的前后指针关联,使得在已知位置插入或删除节点的时间复杂度维持在 O(1)。在鸿蒙操作系统强调的“极致任务调度”和“大屏高刷交互”背景下,利用该数据结构可以显著优化复杂逻辑下的内存抖动与 CPU 空转。 一、

By Ne0inhk