资讯详情

FPGA上的DPCC动态坏点校正:从Bayer滑窗到商用ISP落地

📅 2026/10/20 7:25:02 | 华诺云谱 👁 阅读
FPGA上的DPCC动态坏点校正:从Bayer滑窗到商用ISP落地
做ISP的硬件工程师基本都经历过这样的夜晚传感器增益一拉高预览画面里突然浮出密密麻麻的白点截图放大一看每个点死死钉在固定坐标上不闪烁、不漂移。客户第一反应是Sensor坏了但你把原厂坏点表重新烧一遍画面依然不干净——因为新增的那批全是动态坏点。静态坏点表能覆盖出厂检测时留下的物理缺陷可光电二极管的暗电流、温度漂移、长时间工作后的退化都会让成品上原本健康的像素慢慢“变坏”这些位置不在任何一张出厂表里必须在ISP实时链路上用DPCC动态坏点校正拦下来。我这次要分享的就是在一块FPGA上落地商用级DPCC模块的完整思路Bayer域滑窗怎么搭、多方向并行判决怎么做、误检和漏检怎么平衡、资源和时序怎么控。内容偏硬件向但也适合做系统方案、做Sensor调试的朋友用来理解DPC为什么难。1. 为什么会留下坏点活下来从传感器物理缺陷到ISP的责任边界1.1 静态坏点出厂就带伤靠表就能管光电传感器做完封装测试总会有一小部分像素的表现异常——可能永远输出纯黑、纯白也可能输出值明显偏离正常范围。这类坏点在出厂阶段就会被定位到坐标生成静态坏点表随Sensor出货。ISP端做的事情很简单读表、比对坐标、命中就把像素值替换掉。静态坏点校正之所以能长期有效是因为这些坏点的“坏法”相对稳定亮度响应曲线偏了就是偏了温度变化只是让偏差变大变小性质不会反转成良品。所以一张测出来准的表可以很久不更新。但注意“久”不等于“永远”。我遇到过Sensor工作两三万小时后静态表里某个点失效现场图像上重新冒出坏点的情况。这种情况靠出厂表已经无法兜底正是因为物理不稳定性会持续制造新缺陷。1.2 动态坏点温度、增益与时间都在参与动态坏点的来源可以按三个维度理解。第一是暗电流。光电二极管在无光照时也存在热激发产生的漏电流温度越高漏电流越大输出就会亮。暗场长时间曝光时这个值可能被拉得很高个别管芯的漏电又天生偏大就成了暗场亮点。第二是模拟增益。ISP前端增益把微弱信号放大坏点偏移量同样被放大。同一个像素增益为1倍时偏移淹没在噪声里不可见增益到8倍时就是一个非常显眼的亮斑或暗斑。第三是持续退化。长时间工作、温度循环、光照老化的综合作用会让部分像素发生不可逆改变今天的良品可能变成明天的坏点。这部分根本无法提前入表。从工程角度看动态坏点与静态坏点没有本质差异区别在于“未知出现时机”。因此ISP不能只做查表比对必须在每个像素到达时实时评估它“像不像坏点”这就是DPCC存在的意义。1.3 DPCC在ISP流水线中的位置DPCC通常放在RAW域、去马赛克之前。原因有两个RAW域的像素值保留了传感器最原始的响应坏点特征没有被后面插值/滤波抹平而且Bayer域图像尚未经过颜色插值此时校正坏点不会在插值过程中把坏点信息扩散给周围一圈像素。等到denoise、demosaic都跑完坏点周围已经染上颜色再校就晚了。位置确定之后DPCC模块要面对一个硬约束像素是流式的一个时钟来一个像素整个决策必须在几个像素周期内完成不能回头看几帧也不能丢掉一行数据重建窗口。这也是为什么滑窗架构和并行判决几乎是DPCC的唯一务实解法。这个位置还有一点容易被忽略DPCC不是在YUV域做也不是在RGB域做而是在Bayer RAW域做。Bayer域给DPCC带来的麻烦我在下一章专门展开。2. 滑窗架构中的真实工作量行缓存、同色窗口与像素流同步2.1 Bayer域的坏点比较必须先拆通道Bayer格式下一帧图像RGGB四个相位交错排列R像素和B像素相隔两个像素位置直接取一个3x3窗口中心是R、四邻居是G和B比较上来就是“拿苹果跟橘子比亮度”没有意义。正确做法有两种。一种是把5x5窗口里R、G、B各自对应位置的亮度抽出来形成针对每个相位的同色3x3邻域另一种是硬件上把Bayer数据拆成RG、GB两条数据流分别各自维护窗口。第一种比较通用因为不同Sensor的Bayer排列可能不同代码可配置性更好第二种实现起来更省行缓存但通道分离后需要额外处理两张图拼接回原图时序上更麻烦。我最终采用的是第一种一路像素流进来同时登记行/列计数根据{row[0], col[0]}判断当前像素属于R、Gr、Gb、B中的哪个相位再去记录窗口向量。5x5的窗口实际能拿到5x5共25个像素但对中心像素做同色比较时只用其中间距为2的那些邻居——也就是同相位的3x3。换句话说数据通路维护的是5x5窗口判决逻辑只看中心相位对应的3x3同色矩阵。这样代码里所有比较器的输入永远是同一色彩通道的值。2.2 行缓存资源账4条Line Buffer是怎么算出来的要实现5x5窗口FPGA里必须同时拿着当前行、前一行、前两行、前三行、前四行各一个像素点。像素是流式到达的同一时刻只能看到当前像素要“回头”看到前几行同一列的数据只能靠行缓存。行缓存的组织方式是一个移位链新像素进来压入第0行缓存第0行缓存吐出的旧像素送入第1行缓存依次类推。每个行缓存的深度等于图像水平像素数位宽等于像素位宽。这样在每个时钟沿5行缓存的输出端同时给出同一列上下间隔2行的5个数据配合行内寄存器链就能在下一拍把5x5窗口完整准备好。资源账很好算。以1920x1080、10bit RAW为例每条行缓存是1920*1019200bit4条就是76800bit。按FPGA片上BRAM的单位容量折算通常2到3块BRAM就能覆盖如果分辨率降到1080p以内且对时延敏感也可以用移位寄存器阵列来搭会占用大量FF/LUT但能换来更低的延迟和更高的灵活性。分辨率像素位宽4条行缓存总量约实现建议640x4808bit20Kb分布式RAM/移位寄存器即可1280x72010bit50Kb建议用BRAM约2块按36Kb/块估算1920x108010bit75KbBRAM约2-3块3840x216012bit180KbBRAM约5-6块这里强调一点行缓存深度不是按全分辨率行宽度算的而是按“有效像素”算的。比如1920x1080但带消隐区的时序一行总像素能达到2200甚至更高如果行缓存按2200配会白白多占BRAM按有效宽度配窗口生成时必须在有效区对齐消隐区里的窗口数据要打断。2.3 窗口生成时序与流水线延迟窗口寄存器矩阵5x5的更新逻辑里每一行都用移位寄存器推进像素每拍进来一个窗口整体向右滑动一格行尾时窗口重置。要注意“行有效”与“帧有效”信号参与控制行有效拉低期间窗口不能吸收无效像素否则窗口会被消隐数据污染边界那几列会出现错位。这个模块从像素进来到5x5窗口完整可用会引入至少4个像素周期的行缓存延迟再加上寄存器链的2拍约6-7个像素时钟。对于1080p60约148.5MHz的像素时钟来说不到50ns整条ISP链路完全可以接受。4K60的594MHz像素时钟下窗口生成延迟更小但时序收敛工作量更大这个后面专门讲。窗口生成的Verilog逻辑骨架大致长这样省去行列判相位的完整过程// 5x5窗口寄存器矩阵伪代码示意 wire [DATA_W-1:0] line_in; reg [DATA_W-1:0] row_buf [0:3]; // 4条行缓存读出端口 reg [DATA_W-1:0] win [0:4][0:4]; // 5x5窗口寄存器 always (posedge clk) begin if (px_valid) begin // 行缓存链当前像素进入line_buf0line_buf0旧值进入line_buf1... // 窗口列链每行窗口右侧左移一列最右列填入对应行缓存输出 win[0][4] row_buf[3]; win[1][4] row_buf[2]; win[2][4] row_buf[1]; win[3][4] row_buf[0]; win[4][4] line_in; // 其余列向左移一位 for (int j 0; j 4; j) begin win[0][j] win[0][j1]; // ... 依次类推 end end end伪代码只是示意真实工程里还要处理行有效、帧边界、相位切换时的窗口复位否则边界几列会算出脏数据。3. 并行判决把一个“疑似坏点”在几个时钟内变成过滤结果3.1 多方向梯度计算一个时钟内完成的并行动作窗口准备好之后接下来要回答一个问题中心像素C跟周围邻居比到底异常到什么程度坏点检测最常用的不是“绝对亮度”而是“相对邻居的偏差”。一个白点的绝对亮度可能在暗场里是5000在亮场里是20000直接拿绝对值设阈值没法同时适配两种场景但不管场景多亮坏点与同色邻居的差异都显著大于正常像素之间的差异。所以判决的第一步是计算中心与各个同色邻居的绝对值差。我采用4近邻加4对角线的方式上、下、左、右是距离中心2个像素位置的同色邻居5x5窗口里对应位置另外考虑左上、右下、右上、左下两条对角线方向。每个方向组合做一个绝对值减法wire [DATA_W-1:0] d_up abs(center - up); wire [DATA_W-1:0] d_down abs(center - down); wire [DATA_W-1:0] d_left abs(center - left); wire [DATA_W-1:0] d_right abs(center - right); wire [DATA_W-1:0] d_diag1 abs(center - diag_lu); wire [DATA_W-1:0] d_diag2 abs(center - diag_rd); wire [DATA_W-1:0] d_diag3 abs(center - diag_ru); wire [DATA_W-1:0] d_diag4 abs(center - diag_ld);这八个减法器在硬件上完全是并行的一个时钟周期内出结果。注意Bayer域同色采样时这些邻居在5x5窗口中的坐标不是紧挨着中心而是相隔一行/列因此窗口矩阵里取出来时要按相位索引取对应位置。这一步如果做错后面所有判断全乱。3.2 阈值判决与投票逻辑兼顾灵敏度和误检率有了8个方向差值坏点判决就需要两个阈值配合TH_LOW低阈值用来过滤噪声底线。比它小的差值一律视为正常波动。TH_HIGH高阈值用来判断“显著异常”。差值和它比较决定该方向是否“强烈指认坏点”。一个常用判决规则是至少N个方向的方向差超过TH_HIGH且M个方向超过TH_LOW才判为坏点。比如要求8个方向里至少4个超过TH_LOW且至少2个超过TH_HIGH。这个“投票”逻辑的好处是单一方向的异常比如真实边缘的一侧不会触发坏点判决只有中心像素与周围普遍差一大截才足够可信。投票逻辑在RTL里就是一组比较器和一个加法器/计数器再和阈值比较reg [3:0] cnt_low, cnt_high; always (*) begin cnt_low (d_up TH_LOW) (d_down TH_LOW) ... ; cnt_high (d_up TH_HIGH) (d_down TH_HIGH) ... ; end assign defect (cnt_low LOW_N) (cnt_high HIGH_N);要注意的是计数器的位宽必须能覆盖方向总数避免溢出翻转产生错误判断同时 TH_LOW/TH_HIGH 寄存器建议按像素位宽扩展1到2位防止差值刚好等于阈值时被“等于”错误排除。比较器用大于号阈值本身应理解为“超过才算”。这里还有一个细节阈值并不是一个全局固定值它通常要做成跟随增益的自适应参数。Sensor的模拟增益变化时噪声底和坏点偏移量同步变化阈值要按增益的查表关系调整否则增益一拉高全图都是“坏点”。3.3 替换策略中值滤波为什么是商用首选判决通过后要把坏点替换成合理值。三种常见策略各有取舍策略实现成本边缘保留适用场景邻域均值加法器寄存器成本最低差边缘被抹平快速原型、低端场景邻域中值排序网络比较器数量在数十个量级好适合有结构图像商用主流方向导向替换方向分类选择器最好但判断逻辑复杂高端ISP对细节要求极高我最终选了中值。原因很直接坏点的本质是“这个像素不可信”而中值是邻域排序后的中间值天然丢弃极值对边缘和平坦区域都比较稳均值会把坏点偏向拉低边缘的对比度在暗场里还会让亮点残影一圈。方向导向替换见效最好但需要引入方向分类器逻辑量和验证成本都上去了除非你的产品里DPC后面紧跟着很激进的降噪否则中值已经能解决九成问题。中值实现上4个方向邻居做排序网络大约需要6个比较器扩展到8个邻居时通常在二三十个比较器左右对FPGA来说这点逻辑完全可以接受。排序后取中间值或中间两个值的均值作为替换结果。注意替换不是直接把结果写回原图而是通过MUX在输出级选择pixel_out defect ? median_result : center。这样正常像素一条直线路径过去坏点才走替换路径逻辑延迟差异就控制住了。4. 误判治理实录边缘、纹理、高增益噪声到底怎么应对4.1 真实边缘与坏点的边界在哪里第一版DPC跑出来的误检比想象中多。黑白棋盘格、文字边缘这类高频区域中心像素和一侧邻居的差值天然很大如果不加约束整条边缘都会被当成“坏点列”抹掉。回头看判决条件能发现误检之所以发生是因为“4个方向超过TH_LOW、2个方向超过TH_HIGH”这个投票太粗粒度。在真实边缘上中心与左侧邻居差巨大但中心与右侧邻居差很小只看“计数”不看“方向分布”右侧邻居的小差值照样能满足TH_LOW计数就被凑够了。改进办法有两种我在工程里都试过。第一种是加上“方向一致性约束”如果中心与左邻居差大、与右邻居差也大但与垂直方向邻居差小说明它更像一条横向边缘上的点而不是孤立缺陷应降低坏点置信度。第二种是观察“邻居之间是否也存在类似尺度差异”。坏点往往是单点孤立异常而边缘上相邻像素之间有连续过渡当左右邻居本身也存在大差异时中心更可能是一段真实纹理的一部分。把这层关系编码进判决误检能明显下降。代价是逻辑和验证都变得更复杂需要权衡。4.2 暗场高增益噪声冒充坏点暗场、长曝光、高增益是另一个大坑。像素的读出噪声随增益线性放大单帧RAW里正常像素的随机抖动可能达到几百个LSB比不少真实坏点的偏差还大。此时固定阈值会产生两条结果阈值调高真实坏点漏掉阈值调低整片噪声都被标记成坏点校正后画面出现雪花状的块状闪烁。针对这种情况我建议做三件事。第一把DPC阈值做成随增益变化的查表层。增益越大TH_LOW和TH_HIGH都往上调但两个阈值的比值要保持合理范围避免灵敏度和误检一起失效。这个表建议每个Sensor单独标定不同传感器噪声分布差异很大。第二利用多帧一致性。真实坏点在每一帧的同一坐标都会异常而随机噪声逐帧不同。因此在动态坏点检测结果后追加一个“连续帧确认”逻辑同一坐标累计被判定M次例如3次连续才真正进入校正列表单帧偶发判决不做替换。这样会牺牲一帧左右的时间延迟但换来的稳定性非常高。第三把“像素级判决”和“统计级判决”分开。像素级做的是实时替换统计级做的是候选坏点表的更新。实时替换路径用高阈值保平安统计路径用低阈值捕手发现新坏点两条路互不干扰。这是商用ISP里比较成熟的“快替换、慢学习”结构。4.3 边界行/列与Bayer相位错位边界处理最容易被忽略。图像第一行没有上一行数据第0列没有左侧邻居5x5窗口在四周边界处天然残缺。常见处理有三种边界复制扩展、用部分窗口、直接bypass。我推荐直接bypass即图像前两行、后两行、左两列、右两列不做DPC校正。理由很现实边界像素本来就少坏点出现概率低肉眼在画面边缘也不太敏感复制像素和部分窗口的做法会引入固定的模式反而可能在特定视频里呈现出一条隐约的边线。bypass实现也最简单用行列计数器判断窗口是否完整即可。Bayer相位错位则更隐蔽。RGGB、BGGR、GRBG、GBRG四种排列窗口索引逻辑完全不同。当Sensor驱动配置错误或者输出格式切换时同一套窗口索引取到的“同色邻居”实际是异色像素判决立刻失真。我的办法是在模块入口做一个排列可配置的相位映射表切换Sensor时只改寄存器不改RTL。4.4 动态坏点表学习如何确认一个点是“死透了”最后加一张动态坏点表。当某个坐标在多帧中反复被判定为坏点置信度计数达到一定阈值比如连续8帧就把它写入片上维护的动态坏点表。之后同一坐标出现时直接用表中预留的校正值或标志旁路判决省去实时判决的算力同时锁定该点不会因某一帧噪声翻转。写表前一定要经过确认机制这也是“学习”和“乱学”的分界线。噪声在单帧里偶发大值但在连续多帧内不会一直钉在同一坐标真实坏点则相反。计数器加“连续帧确认”能天然过滤噪声。另一个细节是坏点表要定期衰减某些点可能只是表现为“临时故障”长时间稳定后又会恢复正常如果永远躺在表里反而制造新错误。工程上可以设置观察计时器超时未再命中则自动清除表项。5. 从RTL到商用量产资源、时序与参数化设计5.1 资源预算LUT、FF、BRAM大概要花多少DPCC的硬件成本主要花在四块行缓存BRAM/分布式RAM、窗口寄存器FF、判决逻辑LUT加法器/比较器、替换排序网络比较器选择器。以1080p10bit、5x5窗口、4近邻4对角、中值替换的配置来估算在典型中端FPGA上大约是资源估算用量说明BRAM2-3块4条行缓存按36Kb/块分摊FF约1500个窗口寄存器矩阵流水线寄存器LUT约1200-1800个比较器、计数投票逻辑、排序网络DSP0纯逻辑加减比较不需要乘法器这个量级放到一颗动辄几十万LUT的FPGA里占比非常小说明DPCC真正考验的从来不是面积而是数据流组织和误判治理。有一些优化技巧可以减少资源行缓存按“块内一行”分配时如果像素位宽不是BRAM位宽整数倍可以借位宽打包省块窗口寄存器矩阵里每行不需要完整保存5个像素的情况可以只在当前行保留寄存器链其余行直接从行缓存读出打一拍判决逻辑里计数器和比较器可以复用用时分复用把8个方向分成两组LUT能省一半但会增加一拍延迟。是否值得优化要结合整个ISP链路的时序余量和面积预算判断。5.2 时序收敛与流水线打拍技巧像素时钟到594MHz4K60时组合逻辑必须认真切流水。我的做法是把判决路径拆成四级流水第一级生成窗口和中心像素生成8个方向差。 第二级方向差与TH_LOW、TH_HIGH比较输出8组比较标志。 第三级投票计数得到defect标志。 第四级MUX选择校正结果输出。这样每级组合逻辑大约只有几个LUT的深度再配合寄存器的单一时钟域约束594MHz下依然能稳定收敛。有一处很容易踩坑阈值寄存器从APB/AXI配置接口写入时可能跨越时钟域如果直接进判决比较器会产生亚稳态。配置信号要在模块内做两级同步寄存器打拍并且只在链路空闲/帧间隙生效否则画面会出现一整条或一整块的错误校正带。另外排序网络中值计算本身是一个比较金字塔跨级比较器之间天然有关键路径。如果时序紧张可以给排序网络内部也插入寄存器把中值计算拆成两拍。中值结果晚一个像素到达没关系反正MUX选择时对齐defect标志即可关键是要让defect和median到位的是同一拍。5.3 参数寄存器化与多Sensor配置切换商用ISP必然要面对多Sensor、多分辨率、多场景的切换。DPCC模块不能是写死的硬件所有决策参数都应该暴露成配置寄存器使能与旁路DPCC_EN、BYPASS_EN阈值四件套TH_LOW、TH_HIGH、MIN_LOW_N、MIN_HIGH_N增益自适应阈值表的基址和步进坏点表基址、容量、验证计数阈值排列/相位的映射寄存器这些寄存器建议用双缓冲设计后台写“影子寄存器”调度器在帧间隙统一搬运到“生效寄存器”避免级联配置在帧中间生效导致画面跳变。多Sensor切换时除了配置参数坏点表也要能随Sensor一起切换。如果只有一片SRAM存坏点表不同Sensor的表内容互相覆盖切回原Sensor时坏点表已经失效。我建议给坏点表划分出独立分区或做一套表切换的上下文保存机制。这个细节很多人到量产阶段才会发现改起来比写DPC本身还费劲。5.4 与静态坏点表的双通道融合最终方案里动态DPC和静态坏点表不是二选一而是并行双通道。静态表管“确诊”的坏点坐标命中就直接替换完全不参与动态判决这样可以释放判决算力也避免那些出厂表里的坏点再来干扰投票计数。动态判决管“疑似”的坏点负责发现新缺陷并经多帧确认写入动态表。两条通道在输出级用一个MUX统一选择最终值。这个结构有个附带好处出厂静态表可以做成只读区域动态表做成可写区域两者分开管理。做量产测试时工厂可以重新烧录静态表也方便产线把老化筛选出来的新坏点固化到表中省去专门跑一遍ISP校准流程。最后我把这些年踩坑后的一点经验收个尾。第一版先不要做花哨的方向一致性约束、多帧确认、静态表融合把最朴素的“5x5同色窗口8方向差值阈值投票中值替换”跑通找一张有明显坏点的图验证链路再逐步加约束和自适应。坏点校正在静态图上验证几乎没有意义多准备几组暗场、亮场、高增益、低增益的底片把误检和漏检量化出来再做微调比在RTL里反复试参数高效得多。这也是为什么我始终认为DPCC模块的难点不在RTL本身而在你怎么定义“坏”的标准。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑