4. 坏点校正(DPC)模块设计
坏点校正,简称 DPC,是 ISP 流水线里非常靠前的一步。说白了,就是 sensor 送过来的原始数据里,有些像素点天生就坏了——要么一直亮(白点),要么一直暗(黑点),要么闪个不停。如果不处理,后面做去噪、做 demosaic,这些坏点会被放大,整张图就花了。
我刚开始做 ISP 的时候,觉得 DPC 不就是把坏点替换掉嘛,有什么难的?后来踩了不少坑才发现,怎么检测坏点、用什么策略替换、RTL 怎么做到不拖流水线节奏,这里面的门道还真不少。今天咱们就把这块掰开揉碎了讲清楚。
4.1 坏点检测算法
坏点检测,核心就一句话:判断当前像素是不是“异常”的。怎么判断?两种主流思路:固定阈值法和动态阈值法。
4.1.1 固定阈值法
这个方法最简单,也最直接。你预先设定一个阈值,比如亮点阈值 TH_H 和暗点阈值 TH_L。如果当前像素值大于 TH_H,就认为是亮点;小于 TH_L,就认为是暗点。
固定阈值法判断逻辑:
if (pixel > TH_H) → 亮点
else if (pixel < TH_L) → 暗点
else → 正常点
优点很明显:实现简单,资源消耗小,一个比较器就搞定了。缺点也很致命:阈值是死的,场景一变就不好使了。比如你拍夜景,整体亮度低,正常像素可能也就 30~40,这时候阈值设成 200,暗点根本检测不出来。反过来,大白天拍雪景,像素值普遍高,阈值设低了又容易误判。
适用场景: 光照稳定的工业相机、固定场景的监控摄像头。消费级手机 ISP 基本不用这个,太糙了。
4.1.2 动态阈值法
动态阈值法就聪明多了。它不设死阈值,而是根据当前像素的邻域信息,算出一个“局部阈值”。
具体做法是这样的:取当前像素周围 3x3 或 5x5 的窗口,计算邻域像素的均值 μ 和标准差 σ。然后判断当前像素是否偏离均值太远——比如超过 μ ± k·σ 的范围,就认为是坏点。
我个人习惯用 5x5 窗口 + 3σ 准则。 3x3 窗口太小,容易受噪声干扰;5x5 窗口更稳定,硬件开销也在可接受范围内。k 值一般取 2~4,我项目里常用 3,效果比较均衡。
动态阈值法的核心公式:
μ = (1/N) * Σ(neighbors)
σ = sqrt((1/N) * Σ(neighbor - μ)²)
if |pixel - μ| > k * σ → 坏点
这里有个坑:标准差计算在硬件里挺费资源的,要开方、要乘法。实际工程里,我见过两种简化方案:
- 用差代替标准差: 算邻域均值 μ,然后算每个邻域像素与 μ 的差,取这些差的均值作为“离散度”。虽然不精确,但硬件友好。
- 用最大值最小值范围: 如果当前像素比邻域最大值还大很多,或者比最小值还小很多,就判为坏点。这个更粗暴,但胜在快。
| 方法 | 硬件开销 | 检测精度 | 场景适应性 |
|---|---|---|---|
| 固定阈值法 | 低(1个比较器) | 低 | 差 |
| 动态阈值法(标准差) | 高(需乘加、开方) | 高 | 强 |
| 动态阈值法(差简化) | 中 | 中 | 较强 |
| 动态阈值法(最大最小简化) | 低 | 中低 | 中 |
4.2 坏点替换策略
检测出坏点之后,下一步就是把它替换掉。替换策略直接影响图像质量。常用的有两种:均值替换和中值替换。
4.2.1 均值替换
取坏点周围邻域像素的平均值,填到坏点位置。公式很简单:
pixel_out = (1/N) * Σ(neighbors)
优点:平滑效果好,能有效去除孤立坏点。缺点:如果坏点刚好在边缘附近,均值会把边缘模糊掉。你想想看,一条清晰的边缘上有个坏点,用均值一替换,边缘就糊了。
4.2.2 中值替换
取坏点周围邻域像素的中位数,填到坏点位置。中值的好处是:对边缘保护更好,而且对椒盐噪声(就是那种黑白点噪声)有奇效。
我曾经在一个项目中踩过这个坑: 用均值替换,结果图像边缘细节丢失严重,客户反馈说“照片像油画”。后来换成中值替换,边缘保住了,坏点也去掉了。但中值替换也有代价——硬件里做排序很费资源,3x3 窗口要排 8 个数,5x5 要排 24 个数,面积和功耗都会涨。
实际工程里,我建议这样选:
- 平坦区域多的场景(如天空、墙壁): 用均值替换,效果好,硬件省。
- 纹理丰富的场景(如树叶、织物): 用中值替换,保护细节。
- 折中方案: 先判断当前像素是否在边缘上,如果在,用中值;如果不在,用均值。这个判断本身也需要硬件开销,但效果最好。
4.3 RTL 实现与流水线优化
好了,算法讲完了,咱们聊聊怎么用 RTL 把它实现出来,还要跑得快、不拖流水线节奏。
4.3.1 整体架构
DPC 模块的典型架构分三步:
- 行缓存(Line Buffer): 缓存若干行数据,构建邻域窗口。5x5 窗口需要缓存 4 行。
- 检测逻辑: 从窗口里提取数据,计算均值、标准差或最大最小值,判断当前像素是否为坏点。
- 替换逻辑: 如果是坏点,根据策略计算替换值;如果不是,直通。
下面这张图展示了 DPC 模块的核心数据流:
4.3.2 流水线优化要点
DPC 模块在 ISP 流水线里通常是逐像素处理的,每拍进来一个像素,每拍出去一个像素。要做到这一点,有几个关键点:
- 行缓存用双缓冲或乒乓结构: 一行写、一行读,避免读写冲突。我习惯用双端口 SRAM,读和写可以同时进行。
- 窗口数据用移位寄存器: 每来一个新像素,窗口里的数据整体左移一拍,新像素从右边挤进来,最左边的像素被丢弃。这样每拍都能拿到最新的 5x5 窗口。
- 检测和替换逻辑要组合逻辑实现: 不能引入额外的时钟周期,否则流水线就断了。如果组合逻辑路径太长,可以在中间插入寄存器打一拍,但这样输出会延迟一拍,需要和后面的模块对齐。
- 中值排序用并行比较网络: 3x3 窗口的 8 个邻域像素,用 4 级比较器网络就能排出来,每级都是纯组合逻辑。5x5 窗口的 24 个像素,排序网络会复杂很多,面积也大,所以很多低成本方案只用 3x3。
我个人的经验: 如果芯片面积预算紧张,优先用 3x3 窗口 + 动态阈值(差简化版)+ 中值替换。这个组合在面积和效果之间平衡得最好。我做过一个 1080p@30fps 的 ISP,DPC 模块只用了 8K 门,效果客户很满意。
4.3.3 边界处理
图像边缘的像素,窗口会有一部分跑到图像外面去。怎么处理?常见做法:
- 补零: 窗口外的像素当 0 处理。最简单,但边缘会有伪影。
- 复制边缘: 把最边缘的像素值复制到窗口外。效果比补零好,硬件也容易实现。
- 镜像: 把窗口外的像素按镜像方式取图像内的值。效果最好,但控制逻辑复杂一些。
我建议用复制边缘,性价比最高。补零在 DPC 里效果不好,因为 0 值会被当成暗点,导致边缘误检。
4.4 小结
坏点校正这个模块,说难不难,说简单也不简单。算法层面,动态阈值法比固定阈值法好用得多,但硬件开销要大一些。替换策略上,中值替换对边缘保护更好,但排序网络费资源。RTL 实现的关键是流水线要顺畅,每拍处理一个像素,不能断流。