2、图像处理概述:数字图像在FPGA中的表示与存储格式
各位同学,咱们今天聊聊数字图像在FPGA里到底长什么样。说白了,图像就是一串数字,但怎么组织这些数字,直接决定了你的处理速度和质量。我刚开始用FPGA做图像处理时,就吃过不少亏——比如把像素顺序搞反了,结果整幅图像显示出来是镜像的。嗯,这种坑踩过一次就记住了。
2.1 数字图像的基本概念
一张数字图像,本质上就是一个二维数组。每个位置上的数值,代表该点的亮度或颜色信息。我们管这个最小单元叫像素(Pixel)。
举个例子,一张640×480的灰度图,就是640列、480行,总共307200个像素。每个像素用8位二进制数表示,取值范围0~255。0是纯黑,255是纯白,中间是不同程度的灰。
彩色图像呢?最常见的是RGB格式——每个像素由红、绿、蓝三个分量组成。每个分量通常也是8位,所以一个像素就是24位。你想想看,一张1920×1080的彩色图,光像素数据就有1920×1080×3 ≈ 6.2MB。这还只是一帧。
核心要点:FPGA处理图像时,我们面对的不是“图片”,而是连续不断的像素数据流。每一拍时钟,进来一个像素,出去一个结果。这种流式处理,正是FPGA的强项。
2.2 图像在FPGA中的存储格式
FPGA里没有硬盘,也没有文件系统。图像数据要么存在Block RAM(BRAM)里,要么存在外部DDR SDRAM里。怎么存?这就有讲究了。
2.2.1 逐行扫描存储(Raster Scan)
这是最自然的存储方式。从左上角第一个像素开始,从左到右存完第一行,再存第二行,以此类推。说白了,就是按显示顺序存。
我在项目中遇到过一个问题:摄像头输出的数据就是逐行扫描的,但我要做3×3的卷积滤波。如果只存一行数据,根本凑不齐3行。怎么办?得用行缓存(Line Buffer)。
// 伪代码:3行缓存结构
reg [7:0] line_buf_0 [0:IMG_WIDTH-1]; // 第0行
reg [7:0] line_buf_1 [0:IMG_WIDTH-1]; // 第1行
reg [7:0] line_buf_2 [0:IMG_WIDTH-1]; // 第2行
// 每个时钟周期,数据依次移位
line_buf_2 <= line_buf_1;
line_buf_1 <= line_buf_0;
line_buf_0 <= pixel_in;
这样,任意时刻我都能拿到3行、3列的9个像素,构成一个3×3窗口。嗯,这里要注意:行缓存的深度必须等于图像宽度,否则会串行。
2.2.2 窗口缓存(Window Buffer)
有了行缓存,再配合几个寄存器,就能构建出窗口缓存。窗口大小取决于你的算法——3×3、5×5、甚至7×7。
我个人习惯把窗口缓存做成一个二维寄存器阵列。比如3×3窗口:
reg [7:0] window [0:2][0:2]; // 3行3列
// 每个时钟更新窗口内容
// 新像素从右侧挤入,最左侧像素被丢弃
window[0][2] <= pixel_in; // 新像素进入第0行最右
window[0][1] <= window[0][2]; // 右移
window[0][0] <= window[0][1]; // 右移
// 第1行、第2行类似,但数据来自行缓存
小技巧:如果你用Xilinx的Vivado,可以直接调用FIFO或Shift Register IP核来实现行缓存。比自己写Verilog省事得多,而且资源利用率更高。
2.3 色彩空间的表示
FPGA里处理彩色图像,最常用的是RGB和YCbCr两种格式。
| 色彩空间 | 分量 | 位宽 | 特点 |
|---|---|---|---|
| RGB | R, G, B | 各8位,共24位 | 直接来自摄像头传感器,适合显示 |
| YCbCr 4:4:4 | Y, Cb, Cr | 各8位,共24位 | 亮度与色度分离,适合压缩和边缘检测 |
| YCbCr 4:2:2 | Y, Cb, Cr | Y 8位,Cb/Cr共享8位 | 节省带宽,常用于视频传输 |
为什么FPGA里常用YCbCr?因为很多图像处理算法只关心亮度(Y分量),比如边缘检测、直方图均衡。你想想看,如果直接用RGB,你得同时处理三个通道,计算量翻三倍。而转成YCbCr后,只处理Y通道就够了。
我曾经做过一个项目,要在FPGA里做实时人脸检测。一开始直接用RGB做,结果逻辑资源不够用。后来改成YCbCr,只处理Y分量,资源直接省了60%。
注意:RGB转YCbCr需要乘法器和加法器。虽然公式是固定的,但如果你用浮点运算,资源消耗会很大。我建议用定点数近似,比如把系数放大256倍,最后右移8位。这样只用整数乘加,一个时钟周期就能出结果。
2.4 图像数据在DDR中的组织方式
当图像分辨率超过1080p时,BRAM就不够用了。这时候得用外部DDR。但DDR的访问方式和BRAM完全不同——它有行激活、列选通、预充电这些乱七八糟的时序。
为了高效利用DDR带宽,我们通常把图像数据按Burst(突发)方式存取。一次突发传输可以连续读写多个像素,而不是一个像素一个像素地读。
举个例子,DDR3一次突发长度是8。如果每个像素24位(3字节),一次突发可以读8×3=24字节,也就是8个像素。这样效率最高。
// 地址映射示例:假设图像宽度1920,每像素3字节
// 第y行、第x列的像素地址 = 基地址 + (y * 1920 + x) * 3
// 但为了对齐DDR的Burst边界,通常按行对齐到64字节
嗯,这里有个坑:如果你不按行对齐,跨行读取时会浪费带宽。我曾经调试一个1080p60的显示系统,图像老是撕裂。查了两天才发现,是DDR地址没对齐,导致读一行数据要花两倍的时间。
2.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,从像素到行缓存,再到窗口缓存,最后到DDR存储,每一步都有对应的硬件结构。
这张图其实就说明了一件事:FPGA处理图像,本质上就是把二维的像素矩阵,变成一维的数据流,然后用流水线的方式逐级处理。行缓存和窗口缓存是核心结构,它们让你能在任意时刻拿到邻域像素,这是实现卷积、滤波、边缘检测等算法的基础。
总结一下:
- 图像在FPGA里是流式数据,不是完整的帧
- 行缓存是构建窗口的基础,深度等于图像宽度
- YCbCr比RGB更适合做算法处理,省资源
- DDR存储要注意Burst对齐,否则带宽浪费严重
好了,这一章的内容就到这里。记住这些基础概念,后面讲流水线设计时,你会发现自己已经站在了坚实的地基上。