openISP学习2-DPC(黑电平补偿)和BLC(黑电平补偿)

文章目录

  • [1. DPC-Dead Pixel Correction(坏点校正)](#1. DPC-Dead Pixel Correction(坏点校正))
    • [1.1 DPC算法代码](#1.1 DPC算法代码)
    • [1.2 测试代码](#1.2 测试代码)
  • [2. BLC(黑电平补偿)](#2. BLC(黑电平补偿))
    • 2.1算法代码
    • [2.2 测试代码](#2.2 测试代码)
      • ["bggr" bayer pattern 测试代码](#"bggr" bayer pattern 测试代码)
      • [alpha 系数矫正测试代码](#alpha 系数矫正测试代码)
      • [bayer patter "RGGB" 测试代码](#bayer patter "RGGB" 测试代码)
    • [2.3 真实的平台上是如何做BLC的?](#2.3 真实的平台上是如何做BLC的?)

1. DPC-Dead Pixel Correction(坏点校正)

  • 算法原理

    坏点(Dead Pixel)是传感器中响应异常的像素,其值与周围同色像素差异显著。DPC 通过比较待检测像素与其 4x4 邻域内同色像素的差值来判断是否为坏点,并用插值值替换。由于 Bayer 阵列中相邻位置颜色不同,邻域采样时每隔一个像素取一个(步长为 2),这样确保只比较相同颜色的像素。

  • 邻域布局(同色像素,步长 2)

    复制代码
      p1 .  p2 .  p3
      .  .  .  .  .
      p4 .  p0 .  p5    ← p0 为当前检测像素
      .  .  .  .  .
      p6 .  p7 .  p8
  • 坏点检测条件

    当 p0 与所有 8 个同色邻居的差值均超过阈值 thres 时,判定为坏点:

|p0 - pi| > thres,对所有 i ∈ {1,2,...,8}

  • 替换模式

    • mean 模式: 取 4 个直接邻居(上下左右)的均值:

      p0 = (p2 + p4 + p5 + p7) / 4

    • gradient 模式(推荐): 计算 4 个方向的梯度,选最小梯度方向的两邻均值:

    dv = |2p0 - p2 - p7| (垂直方向)
    dh = |2
    p0 - p4 - p5| (水平方向)

    ddl = |2p0 - p1 - p8| (左对角线)
    ddr = |2
    p0 - p3 - p6| (右对角线)

最小梯度方向 → 取对应两端点均值

1.1 DPC算法代码

  • DPC算法代码
    上面介绍DPC算法mean模式和gradient模式的原理,请对号入座
python 复制代码
class DPC:
    'Dead Pixel Correction'

    def __init__(self, img, thres, mode, clip):
        self.img = img
        self.thres = thres
        self.mode = mode
        self.clip = clip

    def padding(self):
        img_pad = np.pad(self.img, (2, 2), 'reflect')
        return img_pad

    def clipping(self):
        np.clip(self.img, 0, self.clip, out=self.img)
        return self.img

    def execute(self):

        """
        Pixel array in code is showed above:

        p1 p2 p3
        p4 p0 p5
        p6 p7 p8

        it makes sense for calculating follow-up gradients of pixel values (horizontal,vertical,left/right diagonal).
        """
        img_pad = self.padding()
        raw_h = self.img.shape[0]
        raw_w = self.img.shape[1]
        dpc_img = np.empty((raw_h, raw_w), np.uint16) 
        # change uint16 to int_, still exists overflow warning  in the following abs calculation
        for y in range(img_pad.shape[0] - 4):
            for x in range(img_pad.shape[1] - 4):
                p0 = img_pad[y + 2, x + 2].astype(int)
                p1 = img_pad[y, x].astype(int)
                p2 = img_pad[y, x + 2].astype(int)
                p3 = img_pad[y, x + 4].astype(int)
                p4 = img_pad[y + 2, x].astype(int)
                p5 = img_pad[y + 2, x + 4].astype(int)
                p6 = img_pad[y + 4, x].astype(int)
                p7 = img_pad[y + 4, x + 2].astype(int)
                p8 = img_pad[y + 4, x + 4].astype(int)

                if (abs(p1 - p0) > self.thres) and (abs(p2 - p0) > self.thres) and (abs(p3 - p0) > self.thres) \
                        and (abs(p4 - p0) > self.thres) and (abs(p5 - p0) > self.thres) and (abs(p6 - p0) > self.thres) \
                        and (abs(p7 - p0) > self.thres) and (abs(p8 - p0) > self.thres):
                    if self.mode == 'mean':
                        p0 = (p2 + p4 + p5 + p7) / 4
                    elif self.mode == 'gradient':
                        dv = abs(2 * p0 - p2 - p7)
                        dh = abs(2 * p0 - p4 - p5)
                        ddl = abs(2 * p0 - p1 - p8)
                        ddr = abs(2 * p0 - p3 - p6)
                        if (min(dv, dh, ddl, ddr) == dv):
                            p0 = (p2 + p7 + 1) / 2
                        elif (min(dv, dh, ddl, ddr) == dh):
                            p0 = (p4 + p5 + 1) / 2
                        elif (min(dv, dh, ddl, ddr) == ddl):
                            p0 = (p1 + p8 + 1) / 2
                        else:
                            p0 = (p3 + p6 + 1) / 2
                dpc_img[y, x] = p0.astype('uint16')
        self.img = dpc_img
        return self.clipping()

1.2 测试代码

  • mean模式测试代码
    测试时代码会生成一个20x20的raw图,并将重心坐标的设置为黑,然后使用mean模式进行DPC
python 复制代码
    def _make_img_with_dead_pixel(self, h=20, w=20, bg=100, dead_val=0):
        """构造中心有坏点的均匀图像(同色像素背景为 bg,坏点为 dead_val)。"""
        img = np.full((h, w), bg, dtype=np.uint16)
        cy, cx = h // 2, w // 2
        # 确保坐标落在偶数位置(R 通道)
        cy = cy if cy % 2 == 0 else cy - 1
        cx = cx if cx % 2 == 0 else cx - 1
        img[cy, cx] = dead_val
        return img, cy, cx
        
	def test_dead_pixel_corrected_mean_mode(self):
        """mean 模式:坏点应被周围均值替换。"""
        img, cy, cx = self._make_img_with_dead_pixel(bg=100, dead_val=0)
        dpc = DPC(img.copy(), thres=30, mode='mean', clip=1023)
        out = dpc.execute()
        show_bayer_images(img, out, "left", "right") #展示处理前后的图像
  • mean测试效果

    测试代码会生成一个 20x20 的图像,其中每个像素的默认值为 100,并将坏点(bad pixel)的值默认设置为 0

    由于在显示时,我先把raw->RGB.所以下图左侧原图能看到中心元素的8邻域都是浅绿色. 右侧能看到经过mean模式算法的洗礼,bad pixel被修复了.

  • gradient模式测试代码

python 复制代码
    def test_dead_pixel_corrected_gradient_mode(self):
        """gradient 模式:坏点应被梯度方向最小的两邻均值替换。"""
        img, cy, cx = self._make_img_with_dead_pixel(bg=100, dead_val=0)
        dpc = DPC(img.copy(), thres=30, mode='gradient', clip=1023)
        out = dpc.execute() #请参考上面算法源代码
        show_bayer_images(img, out, "left", "right")
  • gradient 测试效果
    下图左侧图片左侧为绿的原因是,在显示时我先做RAW->RGB,R:0和G:100混合在一起就是蓝色. 右侧能看到bad pixel被修复了.

2. BLC(黑电平补偿)

  • 算法原理

    传感器即使在无光照情况下也会输出非零的底噪(暗电流),称为黑电平(Black Level)。BLC 通过对每个颜色通道减去各自的黑电平偏置来消除这一影响。

  • 数学公式

    对每个颜色通道:

    复制代码
      R_out  = R_in  + bl_r
      B_out  = B_in  + bl_b
      Gr_out = Gr_in + bl_gr + α × R_out / 256
      Gb_out = Gb_in + bl_gb + β × B_out / 256

绿色通道(Gr、Gb)额外有一个与 R/B 通道相关的修正项,用于补偿绿色像素受红/蓝像素串扰的影响。α、β 为融合系数(整数,通常为 0 表示不修正)。

2.1算法代码

如上面介绍的原理,核心算法的原理很简单,

python 复制代码
class BLC:
    'Black Level Compensation'

    def __init__(self, img, parameter, bayer_pattern, clip):
        self.img = img
        self.parameter = parameter
        self.bayer_pattern = bayer_pattern
        self.clip = clip

    def clipping(self):
        np.clip(self.img, 0, self.clip, out=self.img)
        return self.img

    def execute(self):
        bl_r = self.parameter[0]
        bl_gr = self.parameter[1]
        bl_gb = self.parameter[2]
        bl_b = self.parameter[3]
        alpha = self.parameter[4]
        beta = self.parameter[5]
        raw_h = self.img.shape[0]
        raw_w = self.img.shape[1]
        blc_img = np.empty((raw_h,raw_w), np.int16)
        if self.bayer_pattern == 'rggb':
            r = self.img[::2, ::2] + bl_r # 不要疑问这里为什么是+,因为这里是一个偏移,即可正可负
            b = self.img[1::2, 1::2] + bl_b
            gr = self.img[::2, 1::2] + bl_gr + alpha * r / 256
            gb = self.img[1::2, ::2] + bl_gb + beta * b / 256
            blc_img[::2, ::2] = r # 从0,0开始行列每隔2个像素,置R通道值
            blc_img[::2, 1::2] = gr # 从[0,1] 开始每隔2个像素置为 Gr
            blc_img[1::2, ::2] = gb # 从[1,0] 开始每隔2个像素置为 Gb
            blc_img[1::2, 1::2] = b # 从 [1,1] 开始每隔2个像素置为 b
        elif self.bayer_pattern == 'bggr':
            b = self.img[::2, ::2] + bl_b
            r = self.img[1::2, 1::2] + bl_r
            gb = self.img[::2, 1::2] + bl_gb + beta * b / 256
            gr = self.img[1::2, ::2] + bl_gr + alpha * r / 256
            blc_img[::2, ::2] = b
            blc_img[::2, 1::2] = gb
            blc_img[1::2, ::2] = gr
            blc_img[1::2, 1::2] = r
        elif self.bayer_pattern == 'gbrg':
            b = self.img[::2, 1::2] + bl_b
            r = self.img[1::2, ::2] + bl_r
            gb = self.img[::2, ::2] + bl_gb + beta * b / 256
            gr = self.img[1::2, 1::2] + bl_gr + alpha * r / 256
            blc_img[::2, ::2] = gb
            blc_img[::2, 1::2] = b
            blc_img[1::2, ::2] = r
            blc_img[1::2, 1::2] = gr
        elif self.bayer_pattern == 'grbg':
            r = self.img[::2, 1::2] + bl_r
            b = self.img[1::2, ::2] + bl_b
            gr = self.img[::2, ::2] + bl_gr + alpha * r / 256
            gb = self.img[1::2, 1::2] + bl_gb + beta * b / 256
            blc_img[::2, ::2] = gr
            blc_img[::2, 1::2] = r
            blc_img[1::2, ::2] = b
            blc_img[1::2, 1::2] = gb
        self.img = blc_img
        return self.clipping()

2.2 测试代码

"bggr" bayer pattern 测试代码

注意这里的bayer patter是BGGR, 下面测试代码一开始把8x8的bayer的所有B,R通道初始化,GB,GR默认为0,所以都会为黑色. 注意参数那一行,bl_r=-50, bl_b= -100,也就意味这所有的R元素都会减少50,所有的B通道都会减100.

python 复制代码
    def test_bayer_pattern_bggr(self):
        """bggr 模式:B 在偶行偶列,R 在奇行奇列。"""
        img = np.zeros((8, 8), dtype=np.int16)
        img[0::2, 0::2] = 300  # B 从0行,0列开始,每隔2个像素赋值为100,也就图中所有B都为300
        img[1::2, 1::2] = 100  # R 从1行,1列开始,每隔2个像素赋值为100,也就图中所有R都为100
        params = [-50, 0, 0, -100, 0, 0]  # bl_r=-50, bl_b=-100
        blc = BLC(img.copy(), params, 'bggr', clip=1023)
        out = blc.execute()
        show_bayer_images(img, out, "left", "right-pattern_bggr")
        self.assertEqual(int(out[0, 0]), 330)   # B + bl_b
        self.assertEqual(int(out[1, 1]), 120)   # R + bl_r
  • 测试效果
    如算法原理介绍的这样,所有的B,R都变暗了,符合算法原理.

alpha 系数矫正测试代码

  • 测试代码
    注意alpha系数对GR,GB影响较大, 如下测试的bayer patter是"rggb ".代码一开始创建8x8的bayer图像,所有通道都为100. 参数通道只配置了β通道的增益.
python 复制代码
    def test_alpha_correction(self):
        """alpha > 0 时,Gr 通道应额外加 alpha * R / 256。
        注意:BLC 中 alpha*r 是 int16 乘法,r_val 需避免乘法溢出(int16 上限 32767)。
        r_val=100, alpha=128 → 128*100=12800(未溢出),修正量=12800/256=50。
        Gr_out = g_val(100) + bl_gr(0) + 50 = 150。
        """
        img = make_bayer_rggb(8, 8, r_val=100, g_val=100, b_val=100)
        params = [0, 0, 0, 0, 128, 0]  # alpha=128
        blc = BLC(img.copy().astype(np.int16), params, 'rggb', clip=2000)
        out = blc.execute()
        show_bayer_images(img, out, "left", "right-alpha-correction")
        #  因为Gr_out = Gr_in + bl_gr + α × R_out / 256
        #  则Gr[0,1] = 100 + 0 + 128*100/256 = 100 + 50 = 150
		#  因为Gb_out = Gb_in + bl_gb + β × B_out / 256
        #  则Gb[1,0] = 100 + 0 + 0*100/256 = 100 + 50 = 100 .注意之类β是0,参数没有指定
  • 测试效果
    这里由于只配置了α的增益,β无增益,所以Gb和R,B一样的颜色,如下图,GR变亮,GB无增益,符合预期.

bayer patter "RGGB" 测试代码

原理和上面的"BGGR" patter是一样的,这里就不多说了.

python 复制代码
    def test_offset_applied_rggb(self):
        """rggb 模式:R 通道加 10,B 通道加 20。"""
        img = make_bayer_rggb(8, 8, r_val=100, g_val=200, b_val=150)
        params = [-10, 0, 0, -20, 0, 0]  # bl_r=-10, bl_b=-20
        blc = BLC(img.copy().astype(np.int16), params, 'rggb', clip=1023)
        out = blc.execute()
        show_bayer_images(img, out, "left", "right-applied_rggb")
  • 测试效果
    目前pattern是"RGGB", 我们在R和B通道上减去了一个值,所以R和B通道的变得更黑了.如下图符合预期.

2.3 真实的平台上是如何做BLC的?

在具体平台上进行 ISP Tuning(图像调优)时,必须采集一张或多张完全黑的图片(即"黑帧 RAW 数据")作为基准数据,以此来计算和提取 BLC(黑电平校正)参数。

在实际的调试流程中,采集黑帧并处理的具体操作和原因如下:

    1. 采集黑帧的严格条件
      为了获取准确的基准数据,必须确保传感器处于绝对无光的环境中。通常的做法是使用镜头盖或遮光黑布完全遮住镜头,阻断任何外部光线干扰。同时,需要将传感器的曝光模式设置为手动模式,并在不同的增益档位(如 1x、2x、4x 等)下分别采集 RAW 数据。
    1. 基准数据的计算与提取
      采集到黑帧 RAW 图后,调优工具(如 RKISP Tuner 或 K230_ISPCalibrationTool)会将图像分离为 R、Gr、Gb、B 四个通道,并分别统计各通道的平均值或中值。这个平均值就是该增益下的黑电平偏移量。工具会自动计算这些值,并将其写入到 IQ(Image Quality)配置文件中。
    1. 为什么需要多张黑帧(ISO联动)
      由于传感器的暗电流受温度和增益(Gain/ISO)影响较大,单一的黑帧往往无法覆盖所有场景。因此,在实际调优中,通常会在初始 ISO 的基础上,通过等差或等比数列的方式增加 ISO,重复采集多张黑帧。系统会将这些二维数据生成一个查找表(LUT),在后续实际成像时,根据当前的 ISO 动态查表或插值,进行精准的 BLC 校正。
    1. 为什么不能直接套用其他图片的数据?
      BLC 是整个 ISP 流水线的第一步,其作用是为后续所有模块建立正确的信号基准。如果 BLC 参数不准确(偏大或偏小),会导致画面整体偏暗、动态范围减小,或者在暗部出现彩色噪点,并且这种错误会级联影响后续的 LSC、AWB 等模块。因此,必须使用专门采集的黑帧作为唯一基准,而不能使用包含实际景物信息的普通图片。
相关推荐
简简单单做算法3 小时前
基于DNA算法的遥感图像加解密matlab仿真
计算机视觉·matlab·dna算法·遥感图像加解密
却道天凉_好个秋3 小时前
HEVC(二):如何实现并行处理
人工智能·算法·计算机视觉·hevc·瓦片技术·波前并行处理wpp
君为先-bey14 小时前
JointDiT:使用扩散变换器增强RGB-深度联合建模
人工智能·深度学习·计算机视觉·扩散模型·图像生成
山居秋暝LS16 小时前
PaddleLabel标注注意事项_完整版
计算机视觉
大模型任我行18 小时前
蚂蚁:无师自通的视觉记忆增强
人工智能·计算机视觉·语言模型·论文笔记
weixin_4684668521 小时前
深度学习图像数据增强新手实战指南
图像处理·人工智能·深度学习·ai·数据增强·机器视觉
简简单单做算法21 小时前
基于混沌加密的遥感图像加密算法matlab仿真
图像处理·计算机视觉·matlab·混沌加密·遥感图像加密
armwind1 天前
openISP学习5-CNF — Chroma Noise Filtering(Bayer 域色度噪声滤波)
图像处理·计算机视觉
armwind1 天前
openISP学习4-AWB(自动白平衡增益控制)
图像处理·计算机视觉