拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA驱动OV5640实现中值滤波:从行缓存到时序收敛的完整工程实践

简介针对FPGA与摄像头图像处理初学者以Xilinx Spartan-6系列XC6SLX16为平台演示如何驱动OV5640 CMOS摄像头并通过Verilog HDL完成图像采集与中值滤波。工程覆盖摄像头寄存器配置、数据捕获、FIFO缓存及3×3窗口滤波等关键模块适合希望掌握FPGA外设驱动、时序设计和实时图像处理流程的开发者参考。压缩包共含755个文件约13.51MB。核心代码以v、vhd等硬件描述语言源文件为主同时包含ISE工程文件、约束文件、综合与仿真脚本以及bit流配置文件和PDF说明基本涵盖从编辑、综合到下载验证的完整工程链。目录按功能划分便于按模块学习。目前已有134人学习。通过学习该工程可掌握OV5640的寄存器配置与数据读取、像素缓存与中值滤波的并行实现以及FPGA工程调试方法。无论入门摄像头采集还是进阶图像算法硬件加速都可获得一套可运行的参考设计。1. 这个项目到底在解决什么FPGA 从摄像头取图再到把图像处理结果送出去这条链路几乎是数字图像处理入门绕不开的贯通训练XC6SLX16 这块 Spartan-6 家族里的中端芯片管脚和 Block RAM 都算不上充裕但刚好能把 OV5640 的 DVP 并行输出接住OV5640 本身又是一颗能出 500 万像素、带 SCCB 配置接口的传感器而中值滤波作为典型的非线性空域滤波恰恰是最适合在 FPGA 上展示“行缓存 滑动窗口 排序网络”这三板斧的算法。三者凑在一起几乎就是把 FPGA 图像处理最常见的工作流——传感器配置、像素同步、行缓存、窗口生成、算法流水线、时序收敛——全部走了一遍。这个项目适合两类人。一类是刚把 Verilog 语法学完、想用一个完整工程把“时钟域”“FIFO”“状态机”串起来的入门者另一类是已经在做 FPGA 开发、但平时只写接口逻辑或只做算法仿真、想看看图像类算法在真实器件上要注意什么资源的工程师。XC6SLX16 属于 Spartan-6 家族本身不新但正因为不新才逼着你把每一块 BRAM、每一个 slice 都算着用这比在超大芯片上堆 IP 更能练出对资源的感觉。下文围绕“驱动 OV5640 采集——做中值滤波——调试与验证”这条主线把能直接落地的做法展开。2. OV5640 驱动与数据采集链路的搭建2.1 先厘清 OV5640 的接口模式和像素格式OV5640 的传感器输出接口有三种DVP 并行、MIPI CSI-2、以及内嵌的 ISP 直接输出 YUV/RGB。这个项目标题限定在“驱动 OV5640 摄像头采集图像”最常见、也最适合 XC6SLX16 的做法是走 DVP 并口。XC6SLX16 上没有硬核 MIPI要用逻辑拼 MIPI RX 的差分接收和 byte align会浪费大量 slice 和时钟资源对一块入门级芯片来说不划算。DVP 接口的核心信号就这几根PCLK像素时钟由传感器输出和像素数据同步VSYNC帧同步高/低电平有效可配HREF行同步高电平期间D[9:2]上的数据有效D[9:2]8/10 位像素数据常用 RGB565 取高 8 位或 YUV422 取 8 位像素格式的选择直接决定后续滤波的复杂度。OV5640 的 ISP 可以输出 RGB565、YUV422、RAW Bayer 等格式。中值滤波本身是灰度域算法对彩色图像做逐通道滤波也可以但 XC6SLX16 的 BRAM 总共只有 32 个 18Kb如果对 RGB 三个通道各做一次行缓存资源会非常紧张。常规做法是让 OV5640 直接输出 YUV422 或灰度格式只取 Y 分量做滤波或者输出 RGB565 后在 FPGA 内先做一个快速灰度转换。2.2 SCCB 寄存器配置别漏掉关键项OV5640 的上电和配置时序有严格要求。SCCB 协议和 I2C 基本兼容地址 8 位写地址是0x42读地址0x43。配置的核心是先给传感器上电等待时钟稳定然后通过 SCCB 写入分辨率、输出格式、时钟分频等寄存器。// SCCB 写时序核心状态机片段 localparam IDLE 3d0; localparam START 3d1; localparam SEND_ADDR 3d2; localparam SEND_REG 3d3; localparam SEND_DATA 3d4; localparam STOP 3d5; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sccb_scl 1b1; sccb_sda 1b1; state IDLE; end else begin case (state) IDLE: begin if (cfg_start) begin state START; bit_cnt 4d0; end end START: begin sccb_sda 1b0; // SCL 高电平期间 SDA 拉低产生起始条件 sccb_scl 1b1; state SEND_ADDR; end SEND_ADDR: begin // 8 位器件地址 0x42逐位发送 sccb_scl 1b0; sccb_sda dev_addr[7 - bit_cnt]; if (bit_cnt 4d8) begin bit_cnt 4d0; state SEND_REG; end else begin bit_cnt bit_cnt 1b1; end end // ... endcase end end这段代码只展示了起始条件和地址发送片段完整的 SCCB 控制器还需要应答位检测、寄存器地址发送、数据发送和停止条件。状态机的关键是把SCL拉低后再改变SDA否则会产生虚假的起始/停止条件这是新手最容易出错的地方——看到配置没生效用 ILA 抓 SCCB 波形基本都是 SDA 变化时机不对。需要特别强调的寄存器配置寄存器地址功能推荐值说明0x3808输出水平分辨率高字节0x05对应 1280 宽度1080p 模式0x3809输出水平分辨率低字节0x0012800x380A输出垂直分辨率高字节0x04对应 1080 高度以下0x380B输出垂直分辨率低字节0x380x438 10800x3810窗口水平偏移0x00裁切窗口影响时序0x3811窗口水平偏移低字节0x10默认即可0x3A02自动曝光目标值0x04影响亮度初值可不动0x4300输出格式控制0x30YUV422Y 在 D[9:2] 高 8 位0x4300是输出格式寄存器如果要从 YUV422 切到 RGB565需要同时修改多个寄存器不能只改这一个。市面上很多现成配置脚本是针对 1080p 15fps 调的如果你用默认的 20MHz PCLK 去跑 1080p会发现 HREF 周期异常、行场中断对不上这往往是分频寄存器0x3804~0x380F没按输出分辨率联动调整导致的。建议第一版直接跑 VGA640x480或 720p把链路调通后再上 1080p。2.3 把 DVP 时序变成干净的像素流传感器输出的VSYNC、HREF、PCLK和 FPGA 内部时钟是异步关系必须做同步处理。XC6SLX16 上的做法是先用 IDDR 原语把PCLK的双沿数据并成单沿或者直接用PCLK作为采集时钟域把像素数据寄存两拍消除亚稳态再跨到系统时钟域。// 用 PCLK 域寄存两拍消除亚稳态 always (posedge pclk or negedge rst_n) begin if (!rst_n) begin pclk_r1 1b0; pclk_r2 1b0; href_r1 1b0; href_r2 1b0; data_r1 8d0; data_r2 8d0; end else begin pclk_r1 pclk_in; pclk_r2 pclk_r1; href_r1 href_in; href_r2 href_r1; data_r1 data_in; data_r2 data_r1; end end打两拍只是最基础的同步手段真正的问题是像素数据和HREF的对齐关系。OV5640 在HREF拉高后第一个有效像素可能有一个时钟的延迟具体延迟在数据手册里有说明但手册上的典型值不一定覆盖所有配置组合。最稳妥的方法是用 ILA 抓一段HREF和D[9:2]的波形肉眼确认数据稳定在哪一拍然后在采集逻辑里做相应的对齐调整。像素流在HREF有效期间每个PCLK进来一个像素需要把这些像素按行组织起来形成后续滤波模块能消费的行数据。这里不急着写行缓存先把“像素有效”信号pixel_valid和pixel_data这两根信号理干净后面的行缓存和滤波窗口都依赖这两个信号。如果这一步没做干净后面调滤波算法时会出现图像“斜切”或“行错位”——本质上就是像素对齐出了问题不是滤波算法本身的问题。3. 中值滤波的算法选型与 Verilog 结构设计3.1 为什么在 FPGA 上选中值滤波而不是均值滤波中值滤波的核心思想是对滑动窗口内的像素灰度值排序取中间值作为输出。它对椒盐噪声黑白像素随机出现有极好的抑制效果同时能保留边缘信息。均值滤波在去除噪声时会模糊边缘低通特性在 FPGA 上实现更简单只需要做累加和移位但图像质量损失明显。中值滤波虽然没有均值滤波那种规则的高效结构但排序网络在硬件上是完全可并行的非常适合 FPGA。在具体实现中要区分几个不同深度的方案选型会直接影响资源消耗方案窗口大小排序方法资源估算适用场景朴素全排序3x39 个数两两比较排序网络约 30-40 个 slice入门学习逻辑清晰行缓存 排序网络3x3分成三个 3 输入排序器 合并约 20-25 个 slice工程常用平衡好列缓存复用3x3每列独立排序滑动时复用比较结果约 15-20 个 slice资源紧张时的优化做5x5 窗口5x5先列排序再行排序100 slice噪声较多时但 XC6SLX16 吃力对 XC6SLX16 来说3x3 窗口中值滤波是最务实的起点。5x5 窗口会消耗大量 slice 和布线资源而 XC6SLX16 的逻辑单元本来就不多留给后续图像处理的空间会被挤占。标题写的是“实现中值滤波”没有特指窗口大小行业默认做法就是 3x3。3.2 行缓存 滑动窗口是标准套路3x3 窗口需要同时拿到第 N-1 行、第 N 行、第 N1 行的同一列数据。像素是一个一个进来的同一时刻只有一个像素点所以要先把前两行数据存下来等第三行数据到来时三行数据才能对齐。这就是“行缓存”的用途。XC6SLX16 上有 32 个 18Kb 的 Block RAM。如果图像宽度是 640用 8 位灰度像素一行数据是 640x8 5120 bit两个行缓存共 10240 bit远小于一个 18Kb BRAM。甚至在 720p 下1280x8 10240 bit两个行缓存也才 20480 bit用两个 BRAM 就能放下。如果做 RGB565 三通道滤波数据量翻倍BRAM 就开始紧张了。// 行缓存核心逻辑用 Xilinx 的原语或者推断式双口 RAM // 下面是一个用 BRAM 推断的行缓存模块示意 module line_buffer #( parameter DATA_WIDTH 8, parameter LINE_WIDTH 640, parameter LINE_NUM 2 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] din, input wire wr_en, output wire [DATA_WIDTH-1:0] dout_line0, // 第 N-1 行输出 output wire [DATA_WIDTH-1:0] dout_line1 // 第 N 行输出 ); reg [DATA_WIDTH-1:0] mem [0:LINE_WIDTH-1]; reg [9:0] wr_addr; reg [9:0] rd_addr; always (posedge clk) begin if (wr_en) begin mem[wr_addr] din; end end always (posedge clk) begin wr_addr (wr_addr LINE_WIDTH-1) ? 0 : wr_addr 1b1; end always (posedge clk) begin rd_addr wr_addr; // 读地址跟踪写地址延迟一拍 end assign dout_line0 mem[rd_addr]; assign dout_line1 mem[wr_addr]; // 写端口直接读出当前数据 endmodule这段代码用数组推断 BRAM写地址循环走完一行后回到 0读地址比写地址延迟一拍读出上一行数据。dout_line1直接从写端口读拿到的是当前正在写入的像素dout_line0是上一行的对应像素。两个行缓存级联起来就能构造三行数据同时有效。注意 BRAM 的读操作有一个时钟延迟rd_addr wr_addr这条语句意味着读地址是当前写地址的延迟版本读出的数据自然就对齐到了正确的行位置。常见的错误是把读地址和写地址同步更新导致行数据错位一拍图像上表现为整体偏色或者行间闪烁。3.3 三个 3 输入排序器 全排序网络拿到三行同一列的数据后需要在一个窗口内凑齐 3x3 共 9 个像素。这 9 个像素分别是第 N-1 行的第 j-1、j、j1 列第 N 行的第 j-1、j、j1 列第 N1 行的第 j-1、j、j1 列。硬件上要生成这个窗口至少需要两级移位寄存器。每一行数据进来后先用两个寄存器暂存前两个像素当第三个像素到来时三列数据同时有效。再加上行缓存提供的三行并行数据合在一起才是真正的 3x3 窗口。// 窗口生成逻辑对每行做列方向的两个像素延迟 reg [7:0] line0_col0, line0_col1, line0_col2; reg [7:0] line1_col0, line1_col1, line1_col2; reg [7:0] line2_col0, line2_col1, line2_col2; always (posedge clk) begin if (pixel_valid) begin line0_col2 line0_col1; line0_col1 line0_col0; line0_col0 dout_line0; // 第 N-1 行当前像素 line1_col2 line1_col1; line1_col1 line1_col0; line1_col0 dout_line1; line2_col2 line2_col1; line2_col1 line2_col0; line2_col0 pixel_data; // 第 N1 行当前像素 end end9 个像素到位后要做全排序。取 9 个数的中值不一定要把 9 个数完全排序只需要第 5 大的数。但为了编码简单和时序清晰工程上常用的是三级排序网络先对每行的 3 个数做排序得到行内 max/mid/min再对三个列做同样的比较。这样 9 个数的中值可以通过如下推导获得// 3 输入排序器 function [7:0] max3; input [7:0] a, b, c; reg [7:0] t; begin t (a b) ? a : b; max3 (t c) ? t : c; end endfunction // 中值 max(min_col_max, mid_col_mid, max_col_min) 的一种变形 // 更通用的做法是调用系统任务 $signed 比较实际上完整的 3x3 中值排序网络有很多种等价写法。最稳妥的工程方案是直接写出 9 个数的冒泡排序比较网络用组合逻辑实现虽然消耗一些 LUT但逻辑清晰不容易出错。XC6SLX16 有 2278 个 slice每个 slice 4 个 LUT做 9 个数全排序大约 30-40 个 slice完全在承受范围内。// 9 个数取中值的完整比较网络节选 wire [7:0] p00 line0_col0, p01 line0_col1, p02 line0_col2; wire [7:0] p10 line1_col0, p11 line1_col1, p12 line1_col2; wire [7:0] p20 line2_col0, p21 line2_col1, p22 line2_col2; // 行内排序 3 组 wire [7:0] row0_min, row0_mid, row0_max; wire [7:0] row1_min, row1_mid, row1_max; wire [7:0] row2_min, row2_mid, row2_max; sort3 u_sort00(.a(p00), .b(p01), .c(p02), .min(row0_min), .mid(row0_mid), .max(row0_max)); sort3 u_sort01(.a(p10), .b(p11), .c(p12), .min(row1_min), .mid(row1_mid), .max(row1_max)); sort3 u_sort02(.a(p20), .b(p21), .c(p22), .min(row2_min), .mid(row2_mid), .max(row2_max)); // 列向取中值 median_of_3 u_final( .a(row0_mid), .b(row1_mid), .c(row2_mid), .dout(filter_out) );这里sort3是一个 3 输入排序子模块内部比较器输出 min/mid/max 三个值。取中值的理论依据是三行的 mid 值中再取中值就能得到 3x3 窗口的真正中值。这个结论成立的前提是行内排序的 mid 是五数中值的一部分数学推导可以查阅相关文献这里直接给出结论——这个结构比全排序少一半多的比较器时序也好收敛。3.4 边缘像素的处理策略3x3 窗口在图像边界处会越界第一行、最后一行、第一列、最后一列的像素无法凑齐完整的 9 个点。常见处理策略有三种策略做法优缺点直接丢弃边界像素不输出缩小输出图像实现最简单图像尺寸变小复制边缘把边界像素复制一份填充缺失位置输出尺寸不变但会引入少量伪影零填充缺失位置填 0实现简单但滤波结果偏暗工程上最常用的是复制边缘。实现方式是在行缓存读出端做判断如果当前行列坐标处于边界就复制相邻像素的数值。这个逻辑增加的资源极少只需要几组多路选择器但能把输出图像尺寸保持完整后续如果要叠加显示或做帧差尺寸统一会很方便。4. XC6SLX16 资源约束与设计取舍4.1 Spartan-6 的逻辑资源和 BRAM 分配XC6SLX16 的具体资源逻辑单元 14579 个也就是约 2278 个 slice每个 slice 4 个 LUT 和 4 个 FFBlock RAM 共 32 个 18Kb合计 576KbDSP48A1 共 32 个。对于图像采集 中值滤波这个任务BRAM 是主要约束slice 用量相对宽裕。做一个精细的资源估算行缓存 2 个每个 640x8 位 5120 bit用 1 个 BRAM共 2 个 BRAM如果做到 1280x720每行 10240 bit仍然 1 个 BRAM 能放下共 2 个 BRAM帧缓存不走 BRAM需要用外部 DDRSpartan-6 要接 DDR2/DDR3 或直接用内部 BRAM 做小规模 FIFO排序网络组合逻辑约 40 sliceSCCB 控制器加 FIFO 控制逻辑约 20 slice总 BRAM 用量约 4 个左右如果加上输出端的行缓冲离 32 个 BRAM 的上限还远。这块芯片真正受限的是时钟频率和 I/O 数量。XC6SLX16 的速度等级如果是 -2PCLK 最高大约能跑到 100-150MHzOV5640 在 1080p 下需要 72MHz PCLK720p 下 48MHz 够用。确保综合后的时序约束里 PCLK 到达采集逻辑的路径不要过长。4.2 数据跨时钟域的实战处理OV5640 的 PCLK 和 FPGA 系统时钟通常是 50MHz 或 100MHz是异步的。整个数据处理链路有两种架构选择方案一全链路用 PCLK 做像素时钟SCCB 配置用系统时钟。像素从采集到滤波再到输出全部在 PCLK 域只在输出端比如 DDR 写入或显示驱动做跨时钟域转换。这个方案简单中值滤波的排序网络都是组合逻辑不需要额外时钟只要寄存器满足建立时间即可。方案二把像素数据先跨到系统时钟域之后所有处理都用系统时钟。这个方案对时序收敛更友好因为系统时钟通常是板上最干净的时钟但需要一个异步 FIFO 来缓冲跨时钟域的数据。// 异步 FIFO 例化Xilinx FIFO Generator 或手写 fifo_generator_0 u_fifo ( .rst(~rst_n), .wr_clk(pclk), .wr_en(pixel_valid), .din(pixel_data), .rd_clk(sys_clk), .rd_en(sys_rd_en), .dout(sys_pixel_data), .full(fifo_full), .empty(fifo_empty) );我一般建议用方案一把跨时钟域点放在最后。原因很简单中值滤波的行缓存本身在 PCLK 域工作读写都在同一个时钟下没有跨时钟域问题如果方案二行缓存要同时处理 PCLK 域写入和 sys_clk 域读出需要把 BRAM 配成伪双口写时钟和读时钟分离资源消耗和复杂度都会上升。只在最终输出到 VGA 或 DDR 时做一个异步 FIFO 过渡这个 FIFO 的深度按一行像素的 1.5 倍留够即可。4.3 时序约束和综合技巧XC6SLX16 是 Xilinx Spartan-6开发环境是 ISE 14.7虽然老但稳定。新建工程时选择芯片型号 XC6SLX16-2CSG324 或 FTG256 封装在综合选项里把 Optimization Goal 设为 SpeedOptimization Effort 设为 High。生成比特流前必须手动添加时序约束文件UCF不是 XDC——Spartan-6 时代还是 UCF。NET pclk TNM_NET pclk_group; TIMESPEC TS_pclk PERIOD pclk_group 48 MHz HIGH 50%; NET vsync_in TIG; NET href_in TIG; NET sccb_sda TIG;第一行约束 PCLK 为 48MHz对应 720p 的像素时钟。TIGTiming Ignore标记在 VSYNC、HREF、SCCB 这些慢速控制线上告诉工具这些信号不需要做严格的时序分析能减少综合器在这些异步信号上花的大量优化时间。VSYNC 和 HREF 的频率很低亚稳态的概率也低加上同步器打两拍后TIG 是安全的。布局布线后要在 ISE 里看时序报告重点关注pclk到采集寄存器的路径 slack 是否为正。如果出现负 slack优先看是不是行缓存的地址逻辑路径太长把地址计数器拆成两个 always 块或者用寄存器打一拍再做比较通常能解决。5. 仿真验证与上板调试的关键点5.1 用 Testbench 构造带椒盐噪声的图像输入写 HDL 最容易犯的错误是一上来就综合上板结果图像花了也不知道是输入问题还是算法问题。严格的做法是先做仿真验证用 $readmemh 或系统任务读入一张图像数据在 Testbench 里模拟 OV5640 的时序PCLK 翻转、VSYNC 拉高、HREF 按行拉高喂给 DUT再把 DUT 的输出写回文件放到 PC 上用 Python 或 MATLAB 画出来看效果。# 用 Python 生成一张带椒盐噪声的灰度图转成 hex 文件供仿真读取 import numpy as np from PIL import Image # 生成 640x480 灰度图并加噪声 img np.random.randint(0, 256, (480, 640), dtypenp.uint8) # 5% 椒盐噪声 noise_mask np.random.rand(480, 640) 0.05 img[noise_mask] np.random.choice([0, 255], sizeimg[noise_mask].shape) with open(img_in.hex, w) as f: for row in img: for pixel in row: f.write(f{pixel:02x}\n)仿真的输出用$fwrite写进img_out.hex再用 Python 读回来显示# 把仿真输出转回图像 import numpy as np from PIL import Image data np.fromfile(img_out.hex, dtypenp.uint8, sep\n) img_out data.reshape(480, 640) Image.fromarray(img_out, L).save(filtered.png)仿真能验证算法正确性但仿真里没有 BRAM 的读写延迟模型偏差也没有 PCLK 抖动所以仿真通过只代表逻辑对。上板后图像如果出现噪点残留大概率是配置的曝光、增益参数让图像本身过曝或过暗导致噪声特征和仿真假设不符。5.2 用 ILA 抓 VSYNC/HREF/像素对齐上板调试时如果输出图像出现规则性的条纹或者色彩混乱第一时间要抓的是一帧图像内 HREF 拉高的行数和每行的像素计数。OV5640 输出的行场信息如果和 FPGA 侧配置的行列数统计不一致说明传感器的配置寄存器没生效或是 SCCB 的写操作在某几个寄存器上失败了。// 上板调试用的行场计数统计 reg [9:0] href_cnt; // HREF 高电平持续计数 reg [15:0] vsync_cnt; // VSYNC 之间的 PCLK 计数 reg [11:0] line_valid_cnt; // HREF 脉动次数 always (posedge pclk or negedge rst_n) begin if (!rst_n) begin href_cnt 0; line_valid_cnt 0; end else if (vsync_pos) begin // 帧起始打印上一帧统计 line_valid_cnt 0; end else if (href_in) begin href_cnt href_cnt 1b1; end else begin if (href_cnt ! 0) begin line_valid_cnt line_valid_cnt 1b1; href_cnt 0; end end end把line_valid_cnt接到 ILA 的探针上和 OV5640 手册标称的行数对比。如果行数少了先查寄存器0x380E和0x380F垂直总行数是否配置正确如果行数对但图像偏的查0x3810/0x3811窗口偏移。这一步能把“传感器配置问题”和“FPGA 逻辑问题”彻底分开。提示SCCB 配置完成后要等待至少 10 帧约 0.5 秒再开始采集因为传感器内部的自动曝光和自动白平衡需要收敛时间。上电瞬间抓到的第一帧往往是花屏或半黑的这是正常现象。5.3 输出效果的客观验证单凭人眼观察判断滤波效果不够严谨我建议在 FPGA 内同时输出滤波前后的灰度直方图统计。用简单逻辑统计 0~255 每个灰度级出现的次数通过串口或者片上逻辑分析仪把直方图数据导出。椒盐噪声的特征是灰度值集中在 0 和 255 两端有过冲峰中值滤波后的直方图应该明显削平这两个端点的峰值而中间灰度分布基本不变。6. 几个能直接提升效果的进阶处理技巧中值滤波本身只是一个模块项目标题要真正“能看”还要配合几个专门针对 XC6SLX16 和 OV5640 的实用技巧。第一个是让 OV5640 的 ISP 直接输出 8 位灰度格式跳过 FPGA 片内的 RGB 转灰度逻辑。OV5640 的寄存器中可以通过0x4300配置 YUV422 格式此时高字节就是 Y 分量FPGA 侧只取高 8 位相当于硬件免费送了一个灰度通道省掉了 RGB 加权平均的乘法器也省掉了 3 通道行缓存。这个技巧能让 BRAM 占用直接减半。第二个技巧是在中值滤波的输出端加一个简单的“若当前像素和周围像素差过大则替换”的后处理条件。中值滤波对随机椒盐噪声有效但对连片的坏点线同一行连续多个像素异常效果有限——因为 3x3 窗口内坏点可能占多数中值本身就被污染了。这时可以在滤波输出后再加一级比较如果当前输出值距窗口内最大和最小值都很远且和九个像素的中值差异超过一个阈值就把输出抑制为前一行同列的值。这个处理对 OV5640 在低照度下偶尔产生的行噪声有奇效代价只是增加 3 个比较器和 1 个寄存器。第三个技巧是用直方图统计来辅助判断场景亮暗并自动切换阈值。具体做法是在 FPGA 里统计一帧图像灰度值的均值存到寄存器里下一帧滤波时用这个均值动态调整 OV5640 的曝光寄存器。OV5640 的自动曝光本身可用但在光线突变场景如摄像头从窗口转到室内收敛很慢FPGA 侧通过 SCCB 写入0x3A0F到0x3A10等曝光步长寄存器能把收敛时间缩短一半以上。XC6SLX16 剩余的逻辑资源足够支撑直方图统计加阈值管理这一小段控制逻辑。// 帧均值计算用累加器在帧内对像素求和帧结束求平均 reg [23:0] sum_pixel; // 720p 下一帧约 92 万像素12 位累加器不够 reg [19:0] frame_cnt; always (posedge pclk or negedge rst_n) begin if (!rst_n) begin sum_pixel 0; frame_cnt 0; end else if (vsync_pos) begin avg_pixel sum_pixel / frame_cnt; sum_pixel 0; frame_cnt 0; end else if (pixel_valid) begin sum_pixel sum_pixel pixel_data; frame_cnt frame_cnt 1b1; end endsum_pixel用 24 位是因为 8 位像素最大值 255 乘以 720p 的 921600 个像素需要约 28 位才保险。用 24 位在极端全白画面下会溢出工程上留 28 位最稳妥。算出的均值可以作为曝光补偿的反馈均值偏低就提高曝光偏高就降低曝光。最后提醒一个 XC6SLX16 特有的坑。Spartan-6 的 BRAM 在初始配置时输出为 0但如果行缓存读出端没有做复位初始化仿真正常、上板就会出现前几行颜色异常。解决方法是给行缓存输出端加一个按行计的复位使能——只有当前行号大于等于 2 时才让滤波输出有效前两行直接让原始像素透传。这个细节往往就是整个工程从“图像发花”到“图像清晰”的最后一步透传方式损失边界效果但人眼基本看不出滤波输出不完整时画面顶部和左边缘的彩色条纹会非常突兀。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门