一种快速简便优秀的全局曲线调整与局部信息想结合的非线性彩色增强算法(多图深度分析和探索)

  本文的分析基于《Adaptive and integrated neighborhood-dependent approach for nonlinear enhancement of color images》一文相关内容,但对其进行了深度的改良。

  我们首先解读或者说翻译下这篇论文。

  论文公布的时间是2005年了,已经是比较久远的了,我第一次看到该论文大概是在2014年,后面在2016年左右实现了该算法,这里还有当时开发留下的记录,居然是除夕左右做的。佩服自己。

  以前没有特别注意到该算法的效果,觉得也就那样吧,所以没有怎么去发挥它,但是,最近再次审视,发现他除了实现实现简单、速度快,而且还具有效果佳、适应性广、不破坏本身就光照好的位置等等众多优点,似乎比目前我看到的低照度增强算法都好。

  算法本身的步骤分为三步,第一步是根据的图像亮度分布建立一个自适应的全局映射函数,这一步大大的提高了图像中暗部像素的像素值,同时也压缩了图像的动态范围。第二步是所谓的自适应对比度增强,根据像素领域内的平均值和像素值本身的比例,做一个映射,提高整体的对比度。后续还有一个步骤是颜色恢复的过程。

  第一步的全局曲线调整如下所示:

  首先计算出彩色图像的亮度值,这个其实可以有很多方式,包括常用的YUV空间的Y通道,HSL空间的L分量,甚至可以使用我提到的对比度保留去色那种方式获取,论文里使用的是最普通的计算公式:

  我们把亮度归一化得到:

  然后我们用下面这个传输函数(映射函数)来将亮度图像进行一个线性的增强:

  其中的参数Z值由图像本身的内容决定,如下所示:

  式中的L表示亮度图像的累计直方图(CDF)达到0.1时的色阶值,也就是说如果亮度图像中90%的像素值都大于150,则Z=1,如果10%或者更多的像素值都小于50,则Z取值为0,否则其他情况Z则根据L的值线性插值。

  稍作分析下,如果Z=0,说明图像中存在大量的偏暗像素,图像有必要变亮一些,如果Z=1,则说明图像已经很亮了,则此时图像无需继续加亮处理。介于两者之间时,我们也就做中和处理。

  那么我们采用的传输函数是否能达到这种需求呢,我们来看下公式(3)的组成和曲线。

  以Z=0为例,如上图2所示,曲线6为公式(3)最后对应的结果。我们看看其组成。公式(3)的第一和第二部分对应图2中的曲线2和3,他们的和得到曲线4。曲线2中在低亮度区域的增强很显著,在高亮度区域逐渐变缓,曲线3是一个线性函数,随亮度增加线性的减少。公式(3)的第三部分对应曲线5,曲线4和曲线5累加后得到曲线6。可见最终的曲线在低亮度区域显著增强亮度,在高亮度区域缓慢增加亮度。当然这里也是可以采用其他具有类似作用的曲线的。

  对于不同的Z值,图像给出了最终的相应曲线,可见,随着Z值的增加,曲线逐渐变为一条直线(不变化),而我们前面的自适应Z值计算的过程也随着图像亮度的增加而增加,也就是说如果图形原本就很亮,则Z值就越大,基本上图像就没什么调整了,这基本上就是自适应了。

  第二步:自适应对比度增强。

  经过第一步处理后,图像的亮度自适应的得到了增强,但是图像的对比度明显减少了。普通的全局对比度增强算法的过程是使亮的像素更亮,暗的像素更暗(似乎和第一步有点相反的感觉),这样图像的动态方范围就更广了,同时由于这种方法在处理不考虑领域的信息,对于那些领域和他只有细微的差异的像素,其细节很难得到有效提升。因此,我们需要考虑局部的对比度增强,这种增强下,不同位置相同像素值得像素在增强后可以得到不同的结果,因为他们一般会有不同的领域像素值。当当前像素值比周边像素的平均值大时,我们增大当前像素值,而当前像素值比周边像素平均值小时,我们减少它的值。这样的处理过程结果值和当前像素值的绝对值没有关系,至于领域信息有关了。这样,图像的的对比度和细节都能得到有效的提升,同时图像的动态范围也有得到有效的压缩。

  通常,一个比较好的领域计算方式是高斯模糊,我们采用的计算公式如下所示:

  式中,S(x,y)为对比度增强的结果。指数E(x,y)如下所示:

  下标Conv表示卷积,注意这里是对原始的亮度数据进行卷积。P是一个和图像有关的参数,如果原始图像的对比度比较差,P应该是一个较大的值,来提高图像的整体对比度,我们通过求原始亮度图的全局均方差来决定P值的大小。

  当全局均方差小于3时(说明图像大部分地方基本是同一个颜色了,对比度很差),此时P值取大值,当均方差大于10时,说明原图的对比度还是可以的,减少增强的程度,均方差介于3和10之间则适当线性增强。

  这个对比度增强的过程分析如下,

  当卷积值小于原始值时,也就是说中心点的亮度大于周边的亮度,此时E(x,y)必然小于1,由于I’(x,y)在前面已经归一化,他是小于或等于1的,此时式(7)

的值必然大于原始亮度值,也就是亮的更亮(这里的亮不是说全局的亮,而是局部的亮)。如果卷积值大于原始值,说明中心点的亮度比周边的暗,此时E(x,y)大于1,导致处式(7)处理后的结果值更暗。

  为了得到更好的对比度增强效果,我们一般都使用多尺度的卷积和增强,因为各个不同的尺度能带来不同的全局信息。一般来说,尺度较小时,能提高局部的对比度,但是可能整体看起来不是很协调,尺度较大时,能获得整体图像的更多信息,但是细节增强的力度稍差。中等程度的尺度在细节和协调方面做了协调。所以,一般类似于MSRCR,我们用不同尺度的数据来混合得到更为合理的结果。

  尺度的大小,我们可以设置为固定值,比如5,20,120等,也可以根据图像的大小进行一个自适应的调整。

  第三步:颜色恢复。

  此步比较简单,一般就是使用下式:

  Lamda通常取值就为1,这样可以保证图像整体没有色彩偏移。

  以上就是论文的主要步骤,按照这个步骤去写代码也不是一件非常困难的事情:

int IM_BacklightRepair(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
    int Channel = Stride / Width;
    if (Channel != 3)                                    return IM_STATUS_INVALIDPARAMETER;
    if ((Src == NULL) || (Dest == NULL))                return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                    return IM_STATUS_INVALIDPARAMETER;

    int Status = IM_STATUS_OK;
    int RadiusS = 5, RadiusM = 20, RadiusL = 120;
    const int LowLevel = 50, HighLevel = 150;
    const float MinCDF = 0.1f;

    int *Histgram = (int *)calloc(256, sizeof(int));
    unsigned char *Table = (unsigned char *)malloc(256 * 256 * sizeof(unsigned char));        //    各尺度的模糊
    unsigned char *BlurS = (unsigned char *)malloc(Width * Height * sizeof(unsigned char));        //    各尺度的模糊
    unsigned char *BlurM = (unsigned char *)malloc(Width * Height * sizeof(unsigned char));
    unsigned char *BlurL = (unsigned char *)malloc(Width * Height * sizeof(unsigned char));
    unsigned char *Luminance = (unsigned char *)malloc(Width * Height * sizeof(unsigned char));
    if ((Histgram == NULL) || (Table == NULL) || (BlurS == NULL) || (BlurM == NULL) || (BlurL == NULL) || (Luminance == NULL))
    {
        Status = IM_STATUS_OUTOFMEMORY;
        goto FreeMemory;
    }

    float Z = 0, P = 0;
    Status = IM_GetLuminance(Src, Luminance, Width, Height, Stride, false);            //    得到亮度分量
    if (Status != IM_STATUS_OK)    goto FreeMemory;

    for (int Y = 0; Y < Height * Width; Y++)        Histgram[Luminance[Y]]++;        //    统计亮度分量的直方图

    float Sum = 0, Mean = 0, StdDev = 0;
    for (int Y = 0; Y < 256; Y++)                    Sum += Histgram[Y] * Y;            //    像素的总和,注意用float类型保存
    Mean = Sum / (Width * Height);                                                    //    平均值
    for (int Y = 0; Y < 256; Y++)                    StdDev += Histgram[Y] * (Y - Mean) * (Y - Mean);
    StdDev = sqrtf(StdDev / (Width * Height));                                        //    全局图像的均方差

    int CDF = 0, L = 0;
    for (L = 0; L < 256; L++)
    {
        CDF += Histgram[L];
        if (CDF >= Width * Height * MinCDF)        break;    //    where L is the intensity level corresponding to a cumulative distribution function CDF of 0.1.
    }
    if (L <= LowLevel)
        Z = 0;
    else if (L <= HighLevel)
        Z = (L - LowLevel) * 1.0f / (HighLevel - LowLevel);                            //    计算Z值
    else
        Z = 1;

    if (StdDev <= 3)                                    //    计算P值,Also, P is determined by the globaln standard deviation  of the input intensity image Ix, y as
        P = 3;
    else if (StdDev <= 10)
        P = (27 - 2 * StdDev) / 7.0f;
    else
        P = 1;

    for (int Y = 0; Y < 256; Y++)                    //    Y表示的是I的卷积值
    {
        for (int X = 0; X < 256; X++)                //    X表示的I(原始亮度值)
        {
            float I = X * IM_INV255;                                                                        //    公式2
            I = (powf(I, 0.75f * Z + 0.25f) + (1 - I) * 0.4f * (1 - Z) + powf(I, 2 - Z)) * 0.5f;            //    公式3
            Table[Y * 256 + X] = IM_ClampToByte(255 * powf(I, powf((Y + 1.0f) / (X + 1.0f), P)) + 0.5f);    //    公式7及8
        }
    }

    Status = IM_GaussBlur(Luminance, BlurS, Width, Height, Width, RadiusS);
    if (Status != IM_STATUS_OK)    goto FreeMemory;
    Status = IM_GaussBlur(Luminance, BlurM, Width, Height, Width, RadiusM);
    if (Status != IM_STATUS_OK)    goto FreeMemory;
    Status = IM_GaussBlur(Luminance, BlurL, Width, Height, Width, RadiusL);
    if (Status != IM_STATUS_OK)    goto FreeMemory;

    for (int Y = 0; Y < Height; Y++)
    {
        unsigned char *LinePS = Src + Y * Stride;
        unsigned char *LinePD = Dest + Y * Stride;
        int Index = Y * Width;
        for (int X = 0; X < Width; X++, Index++, LinePS += 3, LinePD += 3)
        {
            int L = Luminance[Index];
            if (L == 0)
            {
                LinePD[0] = 0;    LinePD[1] = 0;    LinePD[2] = 0;
            }
            else
            {
                int Value = ((Table[L + (BlurS[Index] << 8)] + Table[L + (BlurM[Index] << 8)] + Table[L + (BlurL[Index] << 8)]) / 3);        //    公式13
                LinePD[0] = IM_ClampToByte(LinePS[0] * Value / L);
                LinePD[1] = IM_ClampToByte(LinePS[1] * Value / L);
                LinePD[2] = IM_ClampToByte(LinePS[2] * Value / L);
            }
        }
    }

FreeMemory:
    if (Histgram != NULL)    free(Histgram);
    if (Table != NULL)        free(Table);
    if (BlurS != NULL)        free(BlurS);
    if (BlurM != NULL)        free(BlurM);
    if (BlurL != NULL)        free(BlurL);
    if (Luminance != NULL)    free(Luminance);
}

  为了提高速度,我们构建了一个2维(256*256)大小的查找表,这也是一种很常规的加速方法。

  算法的代码部分似乎需要的解释的部分不多,都是一些常规的处理,也基本就是一步一步按照流程来书写的。我们来看算法的效果。

  由这两幅图的结果我们初步得到这样的结论:一是该算法很好的保护了原本对比度和亮度就非常不错的部分(比如两幅图的天空部分基本上没有什么变化),这比一些其他的基于Log空间的算法,比如本人博客里的MSRCR,全局Gamma校正等算法要好很多,那些算法处理后原本细节很不错的地方会发生较大的变化。这不利于图像的整体和谐性。第二,就是暗部的增强效果确实不错,很多细节和纹理都能更为清晰的表达出来。

  为了更为清晰的表达出步骤1和步骤2的处理能力,我们把亮度图、单独的步骤1的结果图和单独的步骤的结果图,以及步骤1和步骤2在一起的结果图比较如下:

              亮度图                                全局曲线调整(步骤1)    

          局部对比度增强(步骤2)                            步骤1和步骤2综合

  至于如何得到这些中间结果,我想看看代码稍作修改就应该没有什么大问题吧。

  可以看到,步骤1的结果图中有一部分不是很和谐,有块状出现,这个在后续的步骤我们会提及如何处理。

  其实我把这四个图放在一起,我想说的就是经过这么久的阅读论文,我觉得所有的这类算法都是应该是这种框架,全局亮度调整+局部对比度调节。不同的算法只是体现在这两个步骤使用不同的过程,而想MSRCR这类算法则把他们在一个过程内同时实现,这样可能还是没有本算法这样灵活。

经过尝试,这里的第一步全局亮度调整如果使用自动色阶或者直方图均衡化后得到的结果并不是很理想,至于为什么,暂时没有仔细的去想。

  接着我们讨论下这个算法的一些问题及其改进方法。 

问题一:问题我们注意到在上面的图中全局亮度调整后的图中的一些明显的视觉瑕疵在经过和局部对比度增强混合后在最终的合成图中似乎表现得并不是那么夸张,但是这并不表明这个问题可以忽视,我们看一下下面这张图的结果:

  虽然原图的亮度比较低,但是在视觉上原图的可接受程度要比处理后的图更为好,这主要是因为处理后的图在暗处显示出了很多色块和色斑,而这些色斑在原图中是无法直接看到的,经过增强后他们变得非常的突兀,也就是说他们增强的程度过于强烈,这个原因是核心在于步骤的1的全局调整,在图2中我们看到低亮度处的调整曲线十分的陡峭,这也就意味着增强的程度特别高,会出现的一个现象就是哪怕原始的像素只差1个值,在处理后的结果中会相差几十以上,视觉中表现为色块的现象。我们从上述图的(39,335)坐标处取以20*20的放块来观察:

  一种简单的处理方式就是对放大的幅度进行限制,比如一般我们认为,前后处理的结果不应该超过4倍或者其他值,也可以根据图像的内容去自适应设置这个值,这样就能有效的避免原文出现这样的问题,修正的代码如下所示:

int Value = ((Table[L + (BlurS[Index] << 8)] + Table[L + (BlurM[Index] << 8)] + Table[L + (BlurL[Index] << 8)]) / 3);        //    公式13
float Cof = IM_Min(Value * 1.0f / L, 4);
LinePD[0] = IM_ClampToByte(LinePS[0] * Cof);
LinePD[1] = IM_ClampToByte(LinePS[1] * Cof);
LinePD[2] = IM_ClampToByte(LinePS[2] * Cof);

  当Cof上限位不同值时,效果也有所区别,如下面两图所示:

          Cof上限为4                            Cof上限为8

  根据个人的经验,Cof设置为4基本上能在增强效果和瑕疵之间达到一个平衡。

问题二:边缘问题,我们来看下面两幅测试图及其效果:

         原图                       算法一次处理                    算法二次处理后

                原图                         算法二次处理后

  我们注意到,对于这两幅图,大部分的增强效果都是非常不错的,特别是经过二次增强后,算法的细节和饱和度等都比较不错,但是注意到在边缘处,比如小孩的帽子处、圣诞树和天空的边缘处等等明显发黑,也就是说他没有得到增强。这是怎么回事呢,我们以第一幅图为例,我们查看下他的亮度图单独经过步骤1和步骤2后处理结果:

          亮度图                     全局增强图                    局部对比度增强图

  可以看出,全局增强图在边缘处未发现有任何问题,而局部对比度图在边缘处变得特别黑,我们将亮度图减去局部对比度增强后的图得到下图:

  在头发边缘我们看到了明显的白边。我们分析下,在头发边缘处,像素比较暗,进行卷积时,周边是天空或者窗户等亮区域,这样进行卷积时,无论卷积的半径大小是多少,得到的结果必然是平均值大于中心像素值(而且偏离的比较远),根据前述的对比度增强的原则,这个时候头发处就应该变得更黑了,而在远离边缘区,卷积的值不会和中心像素值有如此大的差异,应该对比度增强的程度也不会如此夸张,就出现上述最终的结果,我们在本文第一个贴图的地面影子增强处也用红色线框标注出了连这种现象。

  此现象在很多具有强边缘的图像中出现的比较明显,而对于普通的自然照片一般难以发现,在论文作者提供的素材中似乎未有该现象发生。

  解决这个问题的方案很明显,需要使用那些能够保证边缘不受滤波器影响的或影响的比较少的卷积算法,这当然就是边缘保留滤波器的范畴了,虽然现在边缘滤波器有很多种类型,但是最为广泛的还是双边滤波器、导向滤波等等。我们这里使用导向滤波来试验下是否能对结果有所改进。

在导向滤波中,导向的半径和Eps是影响滤波器最为核心的两个参数,当Eps固定时,半径很小时,图像有一种毛绒绒的感觉,稍大一点半径,则图像能显示出较好的保边效果,在非边缘区则出现模糊效果,而当半径进一步增大时,整个图像的变换比较小,整体有一种淡淡的朦脓感。当半径固定时,Eps较小时,图像较为清晰,随着Eps增加,整个图像就越模糊,到一定程度就和同半径模糊没有什么区别了。经过摸索和多次测试,个人认为半径参数配置为IM_Min(Width, Height) / 50,Eps参数为20时,能取得非常不错的保边效果。此时,我们将前面的三次模糊直接用一个保边代替,能得到下面的处理效果:

  可以明显的看出边缘部分得到了完美的解决,无任何瑕疵出现了。

  使用单个保边滤波代替多尺度的高斯模糊,我偶然在测试一幅图中又发现了另外一个问题,如下所示(只是从原图中截取了部分显示)。

              原图局部             使用单个导向滤波后处理的结果

  看到处理后的图,感觉到非常的失望,这个是怎么回事呢,后面我单独测试这个图后面亮度图对应的导向滤波的结果,发现也是带有明显的纹路感觉的结果。而且尝试了双边、表面模糊等其他EPF滤波器也得到了同样的结果,但是缺意外的发现作者原始的使用多尺度的高斯模糊的结果却相当的不错:

       原始多尺度的处理结果 高斯单尺度处理结果

  于是我们又重新做了个试验,把原始的多尺度也改成单尺度的,并且尺度大小和导向滤波用的相同,得到的结果如上图所示。

  这种分析表明对于这个图像并不是因为我用了导向滤波才导致这个纹路出现的,使用高斯滤波,在单尺度时也一样有问题。但为什么多尺度时这个问题就消失了呢。

  针对这一现象,我做了几个方面的分析,第一,确实存在这一类图像,在正常时我们看不出这种块状,但是当进行模糊或卷积时,这种块状就相当的明显了,哪怕最为平滑的高斯模糊也会出现明显的分块现象,下面是对上述局部原图分别进行了半径10和20的高斯并适当加亮(以便显示):

  所以单尺度的高斯模糊处理后的结果也必然会带有块状,但是当我们使用多尺度时,我们注意到尺度不同时这个色块的边界是不同的,而我们多个尺度之间时相互求平均值,此时原本在一个尺度上分界很明显的边界线就变得较为模糊了,下图是上面两个图求平均后的结果:

  相对来说色块边界要比前面两个图减弱了不少。

  那么对于使用EPF滤波器的过程,我们如果也使用多尺度的方法,能否对结果进行改善呢,我们这样处理,也采用三个尺度的保边滤波,一个比一个大,但是保持Eps的值不变,经过测试,得到的结果如上所示,虽然仔细看还是能看出色块的存在,但是比原来的要好了很多。

  最后,我们还是来简单的谈下算法的优化问题。

  第一个可优化的地方是2维查找表的建立过程,开始以为只有65536个元素的计算,所以查找表顺序是没有怎么仔细考虑的,但是实测,这一块占用的时间还是蛮可观的,有好几毫秒,主要是因为这里的powf是个很耗时的过程,所以我们主要稍微把循环的位置调换一下,就可以减少大量的计算了,如下:

    for (int Y = 0; Y < 256; Y++)                    //    Y表示的I(原始亮度值)
    {
        float InvY = 1.0f / (Y + 0.01f);
        float I = Y * IM_INV255;                                                                        //    公式2
        I = (powf(I, 0.75f * Z + 0.25f) + (1 - I) * 0.5f * (1 - Z) + powf(I, 2 - Z)) * 0.5f;            //    公式3
        for (int X = 0; X < 256; X++)                //    X表示的是I的卷积值
        {
            Table[Y * 256 + X] = IM_ClampToByte(255 * powf(I, powf((X + 0.01f) * InvY, P)) + 0.5f);        //    公式7及8
        }
    }

  然后耗时的部分就是LinePD[0] = IM_ClampToByte(LinePS[0] * Cof);这样的代码了,这个也可以通过定点化处理,并配合SSE优化做处理。

  优化完成后的程序处理1080P的24位图像大概需要50ms。

  最后我们分享几组利用该算法处理图像的结果。

  当然,有些图在本算法处理后还可以加上自动色阶或直方图均衡等操作进一步提升图像的质量。

  至此,关于低照度图像的增强算法,我想我应该不会再怎么去碰他了,也该休息了。

  本文算法的测试例程见 : http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,位于菜单Enhace->BackLightRepair菜单中。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互扯程序

漫画:什么是中台?

在传统IT企业,项目的物理结构是什么样的呢?无论项目内部的如何复杂,都可分为“前台”和“后台”这两部分。

10830
来自专栏bigsai

java后端学习路线建议

你是想要进大厂,还是想进小公司呢? 对于一个普通本科生,很可能真正步入学习的时间不多。并且能够掌握的知识的广度和深度也是有限度的,还要考虑学习环境的影响。要慎重...

30120
来自专栏微信公众号【Java技术江湖】

万万没想到,JVM内存结构的面试题可以问的这么难?

在我的博客中,之前有很多文章介绍过JVM内存结构,相信很多看多我文章的朋友对这部分知识都有一定的了解了。

8540
来自专栏编舟记

Rust 入门 (Rust Rocks)

做区块链的基本几乎没有人不知道 Rust 这门编程语言,它非常受区块链底层开发人员的青睐。说来也奇怪,Rust 起源于 Mazilla,唯一大规模应用就是 Fi...

12030
来自专栏微信公众号【Java技术江湖】

MyBatis迷信者,清醒点!

不要以为MyBatis更方便,如果只是用一些简单1+1=2,MyBatis会很简单,但当业务实体之间存在复杂的关联、继承关系时,MyBatis的结果集映射会就比...

10030
来自专栏bigsai

数据结构于算法—线性表详解(顺序表、链表)

下面用一个图来浅析线性表的关系。可能有些不太确切,但是其中可以参考,并且后面也会根据这个图举例。

13060
来自专栏bigsai

一文多图搞懂数据结构的双链表!

前面讲过线性表中[顺序表和链表].但双向链表无论在考察还是运用中都占有很大的比例,笔者旨在通过本文与读者一起学习分享双链表相关知识。

10350
来自专栏数据和云

Oracle 20c 新特性:SQL 宏支持(SQL Macro)Scalar 和 Table 模式

SQL宏特性,允许开发人员将复杂的处理通过宏定义实现,随后可以在 SQL 中任何位置调用宏。这个特性的实现类似于12c中实现的 Function in SQL ...

13650
来自专栏bigsai

数据结构与算法—栈详解(看完面试考试再也不怕了)

和数组形成的栈有个区别。就是理论上栈没有大小限制(不突破内存系统限制)。不需要考虑是否越界。

11050
来自专栏DotNet Core圈圈

.NET分布式大规模计算利器-Orleans(一)

Orleans是基于Actor模型思想的.NET领域的框架,它提供了一种直接而简单的方法来构建分布式大规模计算应用程序,而无需学习和应用复杂的并发或其他扩展模式...

10840

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励