【ISP图像处理】流程概述及经典算法(附python代码)

一、ISP整体流程概述

        相机成像的完整链路中,首先,通过设备的光学镜片将光聚焦到传感器上,将光信号转为电信号,然后,通过核心的ISP模块对接收的电信号处理输出可视图像信号,再对图像进行存储和显示。其中,ISP主要功能有噪声去除、坏点去除、去马赛克、白平衡、自动曝光控制等,依赖于ISP模块处理后才能在不同的光学条件下都能较好的还原现场细节,可以说ISP技术在很大程度上决定了摄像机的成像质量。

【ISP图像处理】流程概述及经典算法(附python代码)_第1张图片

       一般而言,ISP包含以下子模块:从 sensor 端过来的图像是拜尔图像,经过坏点矫正(dead pixel, DPC),黑电平补偿(black level compensation, BLC),去噪(denoise),镜头矫正(lens shading correction,LSC),白平衡(auto white balance, awb),去马赛克(demosaic),色彩校正(color correction matrix,CCM),HDR/Tone Mapping,gamma矫正,边缘增强(edge enhance,EE)色彩空间变化(color space manipulation, CSM)等输出 YUV( 或者 RGB) 格式的数据, 再通过 I/O 接口传输到 CPU 中处理。

【ISP图像处理】流程概述及经典算法(附python代码)_第2张图片

       当然对于一个优秀的ISP处理器,肯定不仅仅局限与上述模块,还会包含其他功能,例如HDR/Tone Mapping等,但本博客聚焦于这些主要模块,其他模块读者可以自行查阅资料进行了解。

二、分模块介绍及对应算法(含代码)

1. 坏点矫正

从传感器制造的角度来看,由于制造过程的不确定性,如粉尘、制造故障、曝光不完全等,一个图像传感器可能会有一定数量的缺陷像素。

一般来讲,坏点分为三类:第一类是死点,即一直表现为最暗值的点;第二类是亮点,即一直表现为最亮值的点:第三类是漂移点,就是变化规律与周围像素明显不同的像素点。由于图像传感器中CFA的应用,每个像素只能得到一种颜色信息,缺失的两种颜色信息需要从周围像素中得到。如果图像中存在坏点的话,那么坏点会随着颜色插补的过程往外扩散,直到影响整幅图像。因此必须在颜色插补之前进行坏点的消除。

坏点校正功能分为两个步骤:首先检测有缺陷的像素,然后是用插值值替换缺陷。

【ISP图像处理】流程概述及经典算法(附python代码)_第3张图片

检测是否存在换点通常是通过判断中心点P0和周围像素点之间的插值是否超大于某个预定义好的阈值thres来确定。若为坏点,有两种方式:

一种是计算领域像素均值作为坏点P0的值,即P0 = (P2 + P4 + P5 + P7) / 4

【ISP图像处理】流程概述及经典算法(附python代码)_第4张图片

另一种是基于梯度的滤波,首先计算相邻像素在不同方向上的梯度:

dh = |2P0 - P4 - P5|,  dv = |2P0 - P2 - P7|

ddr = |2P0 - P3 - P6|,  ddl = |2P0 - P1 - P8|

计算得到不同方向梯度后,选择最小梯度方向的邻域像素求均值进行插值:

【ISP图像处理】流程概述及经典算法(附python代码)_第5张图片

两种方法实现代码如下:

import numpy as np
import cv2

def dpc_mean(img, thres=30):
    img_pad = np.pad(img, (2, 2), 'reflect')
    raw_h, raw_w = img.shape

    dpc_img = np.empty((raw_h, raw_w), np.uint16) 

    # change uint16 to int_, still exists overflow warning  in the following abs calculation
    for x in range(img_pad.shape[0] - 4):
        for y in range(img_pad.shape[1] - 4):

            p0 = img_pad[x + 2, y + 2].astype(int)
            p1 = img_pad[x, y].astype(int)
            p2 = img_pad[x, y + 2].astype(int)
            p3 = img_pad[x, y + 4].astype(int)
            p4 = img_pad[x + 2, y].astype(int)
            p5 = img_pad[x + 2, y + 4].astype(int)
            p6 = img_pad[x + 4, y].astype(int)
            p7 = img_pad[x + 4, y + 2].astype(int)
            p8 = img_pad[x + 4, y + 4].astype(int)

            if (abs(p1 - p0) > thres) and (abs(p2 - p0) > thres) and (abs(p3 - p0) > thres) \
                    and (abs(p4 - p0) > thres) and (abs(p5 - p0) > thres) and (abs(p6 - p0) > thres) \
                    and (abs(p7 - p0) > thres) and (abs(p8 - p0) > thres):
                p0 = (p2 + p4 + p5 + p7) / 4

            dpc_img[x, y] = p0.astype('uint16')
    img = dpc_img
    img = np.clip(img, 0, 1024).astype(np.float32)
    return img

def dpc_gradient(img, thres=30):
    img_pad = np.pad(img, (2, 2), 'reflect')
    raw_h, raw_w = img.shape

    dpc_img = np.empty((raw_h, raw_w), np.uint16) 

    # change uint16 to int_, still exists overflow warning  in the following abs calculation
    for x in range(img_pad.shape[0] - 4):
        for y in range(img_pad.shape[1] - 4):

            p0 = img_pad[x + 2, y + 2].astype(int)
            p1 = img_pad[x, y].astype(int)
            p2 = img_pad[x, y + 2].astype(int)
            p3 = img_pad[x, y + 4].astype(int)
            p4 = img_pad[x + 2, y].astype(int)
            p5 = img_pad[x + 2, y + 4].astype(int)
            p6 = img_pad[x + 4, y].astype(int)
            p7 = img_pad[x + 4, y + 2].astype(int)
            p8 = img_pad[x + 4, y + 4].astype(int)


            if (abs(p1 - p0) > thres) and (abs(p2 - p0) > thres) and (abs(p3 - p0) > thres) \
                    and (abs(p4 - p0) > thres) and (abs(p5 - p0) > thres) and (abs(p6 - p0) > thres) \
                    and (abs(p7 - p0) > thres) and (abs(p8 - p0) > thres):
                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[x, y] = p0.astype('uint16')
    img = dpc_img
    img = np.clip(img, 0, 1024).astype(np.float32)
    return img

2. 黑电平补偿

       从传感器的特性来看,传感器的最低输出电压为黑电平电压。由于电路设计的原因,黑电平电压不能为零。该电压可以通过将曝光时间和所有增益(模拟和数字)设置为最小值来测量。这导致即使曝光时间和增益设置为最低值,图像也不是纯暗的。所以,为减少暗电流对图像信号的影响,可以采用的有效的方法是从已获得的图像信号中减去参考暗电流信号,那么就可以将黑电平矫正过来: img = img - black_level.

3. 去噪

        研究发现,噪声在ISP流水线各模块中会不断产生、传播、放大、改变统计特性,对图像质量的影响会越来越大,而且越来越不容易控制。例如,镜头矫正的处理方式是在图像的上乘以一个gain,远离中心的地方gain 越大,因此会导致,gain大的位置,噪声也会被放大;而demosaic 对原始信号进行了插值操作,会导致图像的噪声变为结构性噪声。因此处理噪声的基本原则是越早越好,随时产生随时处理,尽可能将问题消灭在萌芽状态。

       目前主流的ISP产品中一般会选择在RAW域、RGB域、YUV域等多个环节设置降噪模块以控制不同类型和特性的噪声。在YUV域降噪的方法已经得到了广泛的研究并且出现了很多非常有效的算法,但是在RAW域进行降噪则因为RAW数据本身的一些特点而受到不少限制。主要的限制是RAW图像中相邻的像素点分别隶属于不同的颜色通道,所以相邻像素之间的相关性较弱,不具备传统意义上的像素平滑性,所以很多基于灰度图像的降噪算法都不能直接使用。又因为RAW数据每个像素点只含有一个颜色通道的信息,所以很多针对彩色图像的降噪算法也不适用。

        目前适合ISP应用的降噪算法还是以经典低通滤波器的改进版本更为常见。这里介绍几种经典滤波器以及其python实现方式(大部分可以调用opencv库中函数实现):

①均值滤波

       均值滤波是典型的线性滤波算法,它是指在图像上对目标像素给一个模板,该模板包括了其周围的临近像素,再用模板中的全体像素的平均值来代替原来像素值。一般其算子可以表示为:

【ISP图像处理】流程概述及经典算法(附python代码)_第6张图片

实现代码如下:

def medianFilter(img):
    return cv2.medianBlur(img, 5)

②中值滤波

        中值滤波的实现原理是把数字图像中一点的值用该点的一个区域的各个点的值的中值代替。中值滤波在某些情况下可以做到既去除噪声又保护图像的边缘,它是一种非线性的去除噪声的方法。实现代码如下:

def meanFilter(img):
    return cv2.blur(img, (5, 5))

③ 高斯滤波

       高斯滤波是一种线性滤波。就是对整幅图像进行加权平均的过程,每个像素点的值都由其本身和邻域内的其它像素值经过加权平均后得到。高斯平滑滤波器对于抑制服从正态分布的噪声非常有效。高斯滤波的算子服从高斯分布:

【ISP图像处理】流程概述及经典算法(附python代码)_第7张图片

计算时将“中心点”作为原点,越接近中心,取值越大,越远离中心,取值越小。

实现代码如下:

def gaussianFilter(img):
    return cv2.GaussianBlur(img, (3, 3), 0)

④双边滤波

        双边滤波在做邻域滤波时的加权系数不仅仅考虑几何距离,而且考虑灰度相似性。其计算公式如下:

其中,f是含噪图像,h是恢复后的图像,c和s分别是度量几何距离与灰度相似性的函数, K^-1是一个标准化系数,一般取高斯核函数。从这个公式可以看出,与中心点越接近,相似度越高,滤波时分配的权重越高。这样可以在去噪的同时更好地保持细节信息。

实现代码如下:

def bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75):
    return cv2.bilateralFilter(img, d, sigmaColor, sigmaSpace)

⑤引导滤波

        引导滤波(导向滤波)是一种图像滤波技术,通过一张引导图,对初始图象(输入图像)进行滤波处理,使得最后的输出图像大体上与初始图象相似,但是,纹理部分与引导图相似。推导过程这篇文章写得很详细,可以参考:图像处理基础(一)引导滤波 - 知乎

实现代码如下:

def guided_filter(I, p, r, eps=0.001):

    height, width = np.shape(I)
    th = 0.00001 * I.max()**2
    kernel_size = (r,r)

    N = cv2.blur(np.ones((height, width)), (5, 5))

    mean_I = cv2.blur(I, kernel_size) / N
    mean_p = cv2.blur(p, kernel_size) / N
    mean_Ip = cv2.blur(I*p, kernel_size) / N
    cov_Ip = mean_Ip - mean_I * mean_p

    mean_II = cv2.blur(I*I, kernel_size) / N
    var_I = mean_II - mean_I * mean_I
    var_I[var_I < th] = th

    a = cov_Ip/(var_I+eps)
    b = mean_p-a*mean_I

    mean_a = cv2.blur(a, kernel_size) / N
    mean_b = cv2.blur(b, kernel_size) / N

    q = mean_a*I + mean_b
    return q

4. 镜头矫正

       对于相机模块,传感器通过镜头接收图像。但在透镜边缘处,光线与透镜光轴夹角较大。光强随距光轴距离的增加而降低。同时,不同颜色的折射率也不同。因此,透镜阴影会产生两种阴影,亮度阴影和色彩阴影。

5. 白平衡

       白平衡与色温相关,用于衡量图像的色彩真实性和准确性。人类视觉系统具有颜色恒常性的特点,不会受到光源颜色的影响,可识别物体并且更正这种色差。实际生活中,不论是晴天、阴天、室内白炽灯或日光灯下,人们所看到的白色物体总是是白色的,这就是视觉修正的结果。但是图像传感器本身并不具有这种颜色恒常性的特点,获取的图像容易受到光源颜色的影响。因此,为了消除光源颜色对于图像传感器成像的影响,自动白平衡功能就是模拟了人类视觉系统的颜色恒常性特点来消除光源颜色对图像的影响,通过摄像机内部的电路调整,改变蓝、绿、红三个通道电平的平衡关系让不同色温光线条件下白色物体,Sensor的输出都转换为更接近白色。白平衡就是调整R/B增益,达到R、G、B 相等。 比较常用的AWB算法有灰度世界、完美反射法、动态阈值法等。下面分别进行介绍:

① 灰度世界

       假设在自然环境中,不论是强光还是弱光照射下,图像的平均反射强度应该是相等的。在其假设下,每个颜色通道的平均值应该相等。这个算法应用了一种平衡器,以红、绿和蓝三个颜色通道的平均值的平均值作为所需的增益来调整平衡。

实现代码如下:

def gray_world(img):
    R = img[:, :, 0] 
    G = img[:, :, 1] 
    B = img[:, :, 2] 

    r_avg = np.mean(R)
    g_avg = np.mean(G)
    b_avg = np.mean(B)

    k = (r_avg + g_avg + b_avg) / 3
    kr = k / r_avg
    kg = k / g_avg
    kb = k / b_avg

    r = R * kr
    g = R * kg
    b = R * kb

    balance_img = cv2.merge([r, g, b])
    out = np.clip(balance_img, 0, 1023).astype(np.float32)

    return out

② 完美反射法

       完美全反射理论假设图像上最亮点就是白点(R+G+B的最大值),并以此白点为参考对图像进行自动白平衡。但是如果依赖ratio值选取而且对亮度最大区域不是白色的图像效果不佳。

实现代码如下:

def perfect_reflect(img):

    R = img[:, :, 0] 
    G = img[:, :, 1] 
    B = img[:, :, 2] 

    sumRBG = R + G + B
    sum_list = sumRBG.flatten().tolist()
    sum_list.sort()
    ratio = 0.9
    count = round(len(sum_list) * ratio)
    T = sum_list[count]
    index = sumRBG > T
    
    KR = np.max(R) / np.mean(R[index])
    KG = np.max(G) / np.mean(G[index])
    KB = np.max(B) / np.mean(B[index])

    ro = R * KR
    go = G * KG
    bo = B * KB
 
    out = cv2.merge([ro, go, bo])
    # out = np.clip(out, 0, 1).astype(np.float32)

    return out

③ 动态阈值法

       原理和完美反射法一样,用白色来作为调整的基色,采用一个动态的阀值来检测白色点,然后计算增益参数,调整图像。其中,白色区域判定遵循如下公式:

其中,Mr, Mb分别是Cr, Cb的平均值,Dr,Db分别是Cr, Cb的方差。

实现代码如下:

def dynamic_threshold(img):
 
    r, g, b = cv2.split(img)
        
    yuv_img = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
    y, u, v = cv2.split(yuv_img)

    m, n = y.shape
    max_y = np.max(y)
    avg_u = np.mean(u)
    avg_v = np.mean(v)

    avg_du = np.sum(np.abs(u - np.mean(u))) / (m * n)
    avg_dv = np.sum(np.abs(v - np.mean(v))) / (m * n)

    bv = abs(u-(avg_u + avg_du * np.sign(avg_u))) 
    rv = abs(v-(1.5 * avg_v + avg_dv * np.sign(avg_v)))
    index = (( bv < 1.5 * avg_du) & (rv < 1.5 * avg_dv))
    
    candidate = y[index].flatten().tolist()
    candidate.sort(reverse=True)
    count = round(len(candidate)*0.1)
    T = candidate[count]

    y1 = y > T
    avg_r = np.sum(r[y1]) / np.sum(0+y1)
    avg_g = np.sum(g[y1]) / np.sum(0+y1)
    avg_b = np.sum(b[y1]) / np.sum(0+y1)

    r_gain = max_y / avg_r
    g_gain = max_y / avg_g
    b_gain = max_y / avg_b

    ro = r * r_gain
    go = g * g_gain
    bo = b * b_gain
    
    out = cv2.merge([ro, go, bo])
    out = np.clip(out, 0, 1).astype(np.float32)
    
    return out

④自动色彩均衡(ACE)方法

该算法考虑了图像中颜色和亮度的空间位置关系,进行局部特性的自适应滤波,实现具有局部和非线性特征的图像亮度与色彩调整和对比度调整,同时满足灰色世界理论假设和白色斑点假设。

原理和算法可参考以下两篇博客:
https://blog.51cto.com/u_13984132/5651065

https://www.cnblogs.com/wangyong/p/9119394.html

6. 去马赛克

        详细原理和方法可参考一下博文:

【ISP图像处理】Demosaic去马赛克概念介绍以及相关方法整理-CSDN博客文章浏览阅读91次。使用彩色滤光器阵列(CFA)的数码相机需要一个去马赛克程序来形成完整的RGB图像。一般的相机传感器都是采用彩色滤光片阵列(CFA)放置在光感测单元上,在每个像素处仅捕获三种原色成分中的一种。去马赛克方法主要关注于复原非常规区域,比如边缘以及纹理。https://blog.csdn.net/royole98/article/details/134317662?spm=1001.2014.3001.5502

        https://zhuanlan.zhihu.com/p/512357230

        ISP之Demosaic_ahd demosaic-CSDN博客

        小话Demosaic(一) - 知乎

7. HDR / Tone Mapping

        详细介绍可参看我另一篇博文。【ISP图像处理】Tone Mapping基础知识及相关算法(附代码)icon-default.png?t=N7T8https://blog.csdn.net/royole98/article/details/134023035?spm=1001.2014.3001.5502

8. 色彩校正

        颜色校正主要为了校正在滤光板处各颜色块之间的颜色渗透带来的颜色误差。AWB已经将白色校准了,CCM就是用来校准除白色以外其他颜色的准确度的。一般颜色校正的过程是首先利用该图像传感器拍摄到的图像与标准图像相比较,以此来计算得到一个颜色校正矩阵(CCM)。在该图像传感器应用的过程中,及可以利用该矩阵对该图像传感器所拍摄的所有图像来进行校正,让色彩贴近现实、饱满、细节突出、清晰度更好。

【ISP图像处理】流程概述及经典算法(附python代码)_第8张图片

9. gamma矫正

       Gamma 校正是图像处理中一种常用的技术,可用于调整图像的亮度和对比度。一般情况下,当用于Gamma矫正的值大于1时,图像的高光部分被压缩而暗调部分被扩展,当Gamma矫正的值小于1时,图像的高光部分被扩展而暗调部分被压缩,Gamma矫正一般用于平滑的扩展暗调的细节。 而人眼是按照gamma < 1的曲线对输入图像进行处理的。

【ISP图像处理】流程概述及经典算法(附python代码)_第9张图片

实现代码如下:

def gamma_correction(img_orig,gamma=1/2.2):

    img_gama = np.power(img_orig.astype(np.float32), gamma)       
    # img_gama = cv2.merge([r_gama, g_gama, b_gama])
    out = (img_gama - np.min(img_gama))/(np.max(img_gama)- np.min(img_gama)) * 255
    
    return out

       不过,除了全局的gamma矫正,还存在局部自适应的gamma矫正。参考《基于二维伽马函数的光照不均匀图像自适应校正算法》,python实现代码如下:

def adaptive_gamma_correction(img):
    img_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
    h = img_hsv[:,:,0]
    s = img_hsv[:,:,1]
    v = img_hsv[:,:,2]
    kernelSize = (5,5)
    q = np.sqrt(2)
    sigma1 = 15
    sigma2 = 80
    sigma3 = 250
    v_gs1 = cv2.GaussianBlur(v, kernelSize, sigma1/q)
    v_gs2 = cv2.GaussianBlur(v, kernelSize, sigma2/q)
    v_gs3 = cv2.GaussianBlur(v, kernelSize, sigma3/q)
    v_gs = (v_gs1 + v_gs2 + v_gs3)/3

    m = np.mean(v_gs)
    gamma = np.power(0.5, (m-v_gs)/m)
    v_out = np.power(v, gamma)
    out_hsv = np.dstack([h,s,v_out])
    out = cv2.cvtColor(out_hsv, cv2.COLOR_HSV2RGB)

    return out

10.边缘增强

       CMOS输入的图像将引入各种噪声,有随机噪声、量化噪声、固定模式噪声等。ISP降噪处理过程中,势必将在降噪的同时,把一些图像细节给消除了,导致图像不够清晰。为了消除降噪过程中对图像细节的损失,需要对图像进行锐化处理,通过滤波器获取图像的高频分量,按照一定的比例将高频部分和原图进行加权求和获取锐化后的图像,还原图像的相关细节。

实现代码如下:

def gaussianEnhance(img):
    gaussian = cv2.GaussianBlur(img, (3, 3), 0)
    result = cv2.addWeighted(img, 1.5, gaussian, -0.5, 0)
    return result

参考资料:

1. 键盘摄影(七)——深入理解图像信号处理器 ISP - 知乎
2. https://github.com/cruxopen/openISP 

3. https://zhuanlan.zhihu.com/p/271995341

4. 双边滤波 Bilateral Filtering - 知乎

5. 图像处理基础(一)引导滤波 - 知乎

6. https://qitiandasheng.blog.csdn.net/article/details/99544395?spm=1001.2014.3001.5502

你可能感兴趣的:(图像处理,信号处理,isp,python,图像处理)