4. RAW数据预处理与坏点校正:静态/动态坏点检测算法(DPC),基于FPGA的流水线实现,以及如何利用Block RAM做行缓存

各位同学,欢迎来到第四章。前面我们聊了RAW数据的格式和Bayer阵列,今天要讲的是ISP流水线上一个非常基础、但又极其关键的环节——坏点校正(DPC)。

你想想看,一块CMOS传感器,几百万甚至上千万个像素点,生产过程中难免会有几个“瑕疵品”。这些坏点如果不处理,拍出来的照片上就会出现固定位置的亮点或暗点,非常影响观感。我当年刚入行时,就遇到过一块Sensor,坏点密密麻麻,差点以为是镜头脏了……后来才发现是DPC没做好。

好,我们直接进入正题。

4.1 坏点类型与检测原理

坏点,说白了就是光电响应异常的像素。主要分两类:

类型 表现 成因
静态坏点 固定位置,始终过亮(白点)或过暗(黑点) 制造缺陷,出厂即存在
动态坏点 特定条件下才出现,比如高温、长曝光 工艺波动、温度漂移

检测的核心思路是什么?很简单——比较。一个正常的像素,它的值应该和周围同颜色的邻居差不多。如果它比周围亮太多或暗太多,那它大概率就是坏点。

核心公式(简化版):

如果 |Pcenter - Pneighbor_avg| > Threshold,则判定为坏点。

这个Threshold怎么定?静态坏点可以用固定的阈值,动态坏点就需要根据图像局部亮度自适应调整。我个人习惯在项目中把静态和动态结合起来用:先用静态表把已知的坏点干掉,再用动态算法处理那些“漏网之鱼”。

4.2 静态坏点检测:查表法

静态坏点检测,说白了就是“记黑名单”。

Sensor出厂时,厂商会提供一个坏点列表(Defect Pixel List),里面记录了每个坏点的坐标。我们在FPGA里把这个列表存成一张查找表(LUT)。

流程很简单:

  1. 每个像素进来,查它的坐标是否在坏点列表中。
  2. 如果在,标记为坏点,进入校正流程。
  3. 如果不在,直接通过。

这里有个坑——坏点列表可能很大。我曾经处理过一个项目,坏点列表有几千个点。如果用纯LUT硬查,资源消耗非常大。我的做法是用Block RAM构建一个哈希表,把坐标映射到RAM地址,这样查一次只需要一个时钟周期。

小技巧: 如果坏点列表是固定的,可以在FPGA初始化时从外部Flash加载到BRAM里。这样既节省逻辑资源,又方便后期更换Sensor型号。

4.3 动态坏点检测:基于邻域统计

动态坏点检测不需要预存列表,它实时分析像素的邻域信息。常用的方法有:

  • 中值法: 计算3x3邻域内同色像素的中值,如果中心像素与中值的差超过阈值,判为坏点。
  • 均值法: 计算邻域均值,比较差值。
  • 梯度法: 计算水平、垂直、两个对角线方向的梯度,如果所有方向的梯度都很大,说明这个点很“突兀”。

我个人比较推荐中值法。为什么?因为均值法容易被其他坏点“污染”,而中值法对异常值不敏感,鲁棒性更好。

来看一个3x3邻域的例子(以Bayer阵列的G通道为例):

// 假设3x3窗口,中心是G像素
// 周围四个G像素:G00, G02, G20, G22
// 中心:G11

// 计算四个邻居的中值
G_neighbors[4] = {G00, G02, G20, G22};
sort(G_neighbors, 4);
median = (G_neighbors[1] + G_neighbors[2]) / 2;  // 偶数个取平均

// 判断
if (abs(G11 - median) > TH_DPC) {
    is_bad = 1;
} else {
    is_bad = 0;
}

注意: 阈值TH_DPC不能是固定值。在暗光区域,噪声本身就大,固定阈值容易误判。我建议用局部均值乘以一个比例系数作为自适应阈值。比如:TH_adaptive = K * local_mean,K一般在2~4之间。

4.4 坏点校正:插值替换

检测到坏点之后,怎么修?很简单——用邻居的值把它“填”上。

常用的校正方法:

  • 邻居均值: 用周围同色像素的平均值替换坏点。
  • 中值替换: 用中值替换,抗干扰能力更强。
  • 梯度导向插值: 沿边缘方向插值,保留细节。

在FPGA里,我一般用中值替换。原因很简单——实现起来最省资源,效果也够用。毕竟DPC只是ISP流水线的第一步,后面还有去噪、Demosaic等更精细的处理,这里不需要做得太复杂。

4.5 基于FPGA的流水线实现

好,理论讲完了,我们来看看怎么在FPGA里把它跑起来。

整个DPC模块的流水线分为三级:

流水线级 功能 时钟周期
Stage 1 行缓存与窗口生成 1 clk
Stage 2 坏点检测(静态+动态) 1 clk
Stage 3 坏点校正与输出 1 clk

每级流水线之间用寄存器打拍,保证数据同步。整个模块的延迟只有3个时钟周期,非常高效。

下面这张图展示了DPC模块的整体架构:

DPC模块流水线架构图 RAW数据输入 Stage 1 行缓存与窗口生成 (BRAM行缓存) Stage 2 坏点检测 (静态+动态) Stage 3 坏点校正 (中值替换) 静态坏点表 (BRAM LUT) 自适应阈值 (局部均值×K) 说明:整个流水线延迟仅3个时钟周期,每时钟可处理1个像素。 静态表与动态阈值并行工作,通过OR逻辑合并检测结果。

4.6 利用Block RAM做行缓存

行缓存是FPGA实现窗口运算的核心。要生成一个3x3的窗口,我们需要同时访问三行数据。但Sensor的数据是一行一行串行进来的,怎么办?

答案就是——用Block RAM做行缓存。

具体做法:

  1. 例化两个BRAM,每个深度等于图像一行像素数。
  2. 数据流进来时,依次写入BRAM0、BRAM1。
  3. 同时从BRAM0、BRAM1和当前输入中读取数据,拼成3行。
  4. 再用三个移位寄存器,从三行中提取出3x3的窗口。

来看一段简化的Verilog代码:

// 行缓存模块(简化版)
module line_buffer #(
    parameter WIDTH = 12,    // 数据位宽
    parameter COLS  = 1920   // 每行像素数
)(
    input  clk,
    input  rst_n,
    input  [WIDTH-1:0] din,
    input  valid_in,
    output [WIDTH-1:0] dout_row0,  // 上一行
    output [WIDTH-1:0] dout_row1,  // 当前行
    output [WIDTH-1:0] dout_row2   // 下一行(实际是当前输入)
);

    // 例化两个BRAM
    reg [WIDTH-1:0] bram0 [0:COLS-1];
    reg [WIDTH-1:0] bram1 [0:COLS-1];

    reg [10:0] wr_addr;
    reg [10:0] rd_addr;

    always @(posedge clk) begin
        if (!rst_n) begin
            wr_addr <= 0;
        end else if (valid_in) begin
            bram0[wr_addr] <= din;      // 写入BRAM0
            bram1[wr_addr] <= bram0[rd_addr]; // BRAM0读出写入BRAM1
            wr_addr <= wr_addr + 1;
            rd_addr <= rd_addr + 1;
        end
    end

    // 输出三行数据
    assign dout_row0 = bram1[rd_addr];  // 上上行
    assign dout_row1 = bram0[rd_addr];  // 上一行
    assign dout_row2 = din;             // 当前行

endmodule

经验之谈: 对于1080p的图像,一行1920个像素,每个像素12bit,一个BRAM只需要1920×12=23,040bit。Xilinx 7系列的一个BRAM是36Kb,完全够用。两个BRAM就能搞定3行缓存。

有了三行数据之后,再用三个移位寄存器(每个深度为3)就能提取出3x3的窗口了。这部分实现比较简单,我就不贴代码了,留给大家当练习。

4.7 避坑指南与工程经验

最后,分享几个我在项目中踩过的坑:

  • 边界处理: 图像的第一行和最后一行,第一列和最后一列,没有完整的3x3邻域。我的做法是直接复制边界像素,或者干脆不做DPC,让它们原样通过。反正边界像素占比很小,不影响整体效果。
  • 阈值调优: 动态坏点的阈值K值,我建议在仿真阶段用多张不同亮度的图片测试。K值太小会误判,K值太大会漏检。我一般取2.5~3.0之间。
  • 流水线平衡: 如果静态表查表延迟比动态检测多一个周期,记得在动态检测路径上插入一级寄存器做对齐。否则数据会“打架”。
  • 资源复用: 如果后面还有去噪模块(比如Bilateral Filter),它也需要行缓存。可以考虑把DPC和去噪的行缓存共用,节省BRAM资源。

曾经有个项目: 我为了省资源,把DPC的阈值设成了固定值。结果在暗光场景下,大量正常像素被误判为坏点,画面出现“麻子脸”。后来改成自适应阈值,问题才解决。所以,阈值自适应不是可选项,是必选项。

好了,这一章的内容就到这里。DPC虽然基础,但它是ISP流水线的第一道关卡。如果这里没做好,后面的去噪、Demosaic都会放大坏点的影响。希望大家动手把代码写一写,在FPGA上跑通一个简单的DPC模块,你会对流水线设计有更深的理解。