SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

  这是一篇2010年比较古老的文章了,是在QQ群里一位群友提到的,无聊下载看了下,其实也没有啥高深的理论,抽空实现了下,虽然不高大上,还是花了点时间和心思优化了代码,既然这样,就顺便分享下优化的思路和经历。

  文章的名字为:Contrast image correction method,由于本人博客的后台文件已经快超过博客园所容许的最大空间,这里就不直接上传文章了,大家可以直接点我提供的链接下载。

  文章的核心就是对普通的伽马校正做改进和扩展,一般来说,伽马校正具有以下的标准形式:

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

  其中I(i,j)为输入图像,O(i,j)为输出图像,γ为控制参数,当γ大于1时,图像整体变亮,当γ小于1大于0时,图像整体变暗,γ小于0算法无意义。  

  这个算法对于图像整体偏暗或整体偏亮时,通过调节参数γ可以获得较为满意的效果,但是如果图像中同时存在欠曝或过曝的区域,同一个参数就无法同时满意的效果了,因此,可引入一种γ随图像局部区域信息变化的算法来获取更为满意的效果,一种常用的形式如下:

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

  Moroney在其论文Local colour correction using nonlinear masking提出了如下公式:

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

   其中的mask获取方式为:先对原图进行反色处理,然后进行一定半径的高斯模糊。

  这样做的道理如下:如果mask的值大于128,说明那个点是个暗像素同时周边也是暗像素,因此γ值需要小于0以便将其增亮,mask值小于128,对应的说明当前点是个较亮的像素,且周边像素也较亮,mask值为128则不产生任何变化,同时,mask值离128越远,校正的量就越大,并且还有个特点就是纯白色和纯黑色不会有任何变化(这其实也是会产生问题的)。

  如下图所示,直观的反应了不同的mask值的映射结果。

SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。  

  简单写一段测试代码,看看这个的效果如何:

int IM_LocalExponentialCorrection(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) { unsigned char *Mask = (unsigned char *)malloc(Height * Stride * sizeof(unsigned char)); IM_Invert(Src, Mask, Width, Height, Stride); // Invert Intensity
    IM_ExpBlur(Mask, Mask, Width, Height, Stride, 20);        // Blur 
    for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePD = Dest + Y * Stride; unsigned char *LinePM = Mask + Y * Stride; for (int X = 0; X < Width; X++) { LinePD[0] = IM_ClampToByte(255 * pow(LinePS[0] * IM_INV255, pow(2, (128 - LinePM[0]) / 128.0f)));        // Moroney论文的公式
            LinePD[1] = IM_ClampToByte(255 * pow(LinePS[1] * IM_INV255, pow(2, (128 - LinePM[1]) / 128.0f))); LinePD[2] = IM_ClampToByte(255 * pow(LinePS[2] * IM_INV255, pow(2, (128 - LinePM[2]) / 128.0f))); LinePS += 3;    LinePD += 3;    LinePM += 3; } } free(Mask); return IM_STATUS_OK; }

  基本按照论文的公式写的代码,未做优化,测试两张图片看看。

SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。   SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

          原图1                                 Moroney论文的结果

  似乎效果还不错。

  作为一种改进,Contrast image correction method一文作者对上述公式进行了2个方面的调整,如下所示:

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

  第一,高斯模糊的mask使用双边滤波来代替,因为双边滤波的保边特性,这样可以减少处理后的halo瑕疵。这没啥好说的。

  第二,常数2使用变量α代替,并且是和图像内容相关的,具体算式如下:

  当图像的整体平均值小于128时,使用SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。计算,当平均值大于128时,使用SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。计算,论文作者给出了这样做的理由:对于低对比度的图像,应该需要较强烈的校正,因此α值应该偏大,而对于有较好对比度的图,α值应该偏向于1,从而产生很少的校正量。

  对于第二条,实际上存在很大的问题,比如对于我们上面进行测试的原图1,由于他上半部分为天空,下半部分比较暗,且基本各占一般,因此其平均值非常靠近128,因此计算出的α也非常接近1,这样如果按照改进后的算法进行处理,则基本上图像无什么变化,显然这是不符合实际的需求的,因此,个人认为作者这一改进是不合理的,还不如对所有的图像该值都取2,靠mask值来修正对比度。

  那么对于彩色图像,我们有两种方法,一种是直接对RGB各分量处理,如上面的代码所示,另外一种就是把他转换到YCBCR或者LAB或者YUV等空间,然后只处理亮度通道,最后在转换到RGB空间,那么本文对我的有用的帮助就是提供了一个恢复色彩饱和度的方法。一般来说在对Y分量做处理后,再转换到RGB空间,图像会出现饱和度一定程度丢失的现象,看上去图像似乎色彩不足。如下图中间图所示,因此,论文提出了下面的修正公式:

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

  经测试,这样处理后的图色彩还是很鲜艳的,和直接三通道分开处理的差不多(直接三通道分开处理有可能会导致严重偏色,而只处理Y则不会)。

  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。  SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

           原图                直接处理Y通道再转换到RGB空间                                                改进后的效果

  我们贴出按照上述思路改进后的代码:

int IM_LocalExponentialCorrection(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
    unsigned char *OldY = NULL, *Mask = NULL, *Table = NULL;
    OldY = (unsigned char *)malloc(Height * Width * sizeof(unsigned char));
    Mask = (unsigned char *)malloc(Height * Width * sizeof(unsigned char));
    IM_GetLuminance(Src, OldY, Width, Height, Stride);            //    得到Y通道的数据
    IM_GuidedFilter(OldY, OldY, Mask, Width, Height, Width, IM_Max(IM_Max(Width, Height) * 0.01, 5), 25, 0.01f);    //    通过Y通道数据处理得到255-Mask值
    unsigned char *NewY = Mask;
    for (int Y = 0; Y < Height * Width; Y++)
    {
        NewY[Y] = IM_ClampToByte(255 * pow(OldY[Y] * IM_INV255, pow(2, (128 - (255 - Mask[Y])) / 128.0f)));
    }

    for (int Y = 0; Y < Height; Y++)
    {
        unsigned char *LinePS = Src + Y * Stride;
        unsigned char *LinePD = Dest + Y * Stride;
        unsigned char *LinePO = OldY + Y * Width;
        unsigned char *LinePN = NewY + Y * Width;
        for (int X = 0; X < Width; X++, LinePS += 3, LinePD += 3, LinePO++, LinePN++)
        {
            int Old = LinePO[0], New = LinePN[0];
            if (Old == 0)
            {
                LinePD[0] = 0;    LinePD[1] = 0;    LinePD[2] = 0;
            }
            else
            {
                LinePD[0] = IM_ClampToByte((New * (LinePS[0] + Old) / Old + LinePS[0] - Old) >> 1);
                LinePD[1] = IM_ClampToByte((New * (LinePS[1] + Old) / Old + LinePS[1] - Old) >> 1);
                LinePD[2] = IM_ClampToByte((New * (LinePS[2] + Old) / Old + LinePS[2] - Old) >> 1);
            }
        }
    }
    free(OldY);
    free(Mask);
    return IM_STATUS_OK;
}

  代码并不复杂,基本就是按照公式一步一步编写的,其中IM_GetLuminance和IM_GuidedFilter为已经使用SSE优化后的算法,对于本文一直使用的测试图675*800大小的图,测试时间大概再40ms,而上述两个SSE的代码耗时才5ms不到,因此,可以进一步优化。

  第一个需要优化的当然就是那个NewY[Y]的计算过程了,里面的pow函数是非常耗时的,仔细观察算式里只有两个变量,切他们都是[0,255]范围内的,因此建立一个256*256的查找表就可以了,如下所示:

    Table = (unsigned char *)malloc(256 * 256 * sizeof(unsigned char));
    for (int Y = 0; Y < 256; Y++)
    {
        float Gamma = pow(2, (128 - (255 - Y)) / 128.0f);
        for (int X = 0; X < 256; X++)
        {
            Table[Y * 256 + X] = IM_ClampToByte(255 * pow(X * IM_INV255, Gamma));
        }
    }
    
    for (int Y = 0; Y < Height * Width; Y++)
    {
        NewY[Y] = Table[Mask[Y] * 256 + OldY[Y]];
    }
   free(Table);

  速度一下子跳到了15ms,由于是查表,基本上无SSE优化的发挥地方。

  接着再看最后的饱和度校正部分的算法,核心代码即:

    LinePD[0] = IM_ClampToByte((New * (LinePS[0] + Old) / Old + LinePS[0] - Old) >> 1);
    LinePD[1] = IM_ClampToByte((New * (LinePS[1] + Old) / Old + LinePS[1] - Old) >> 1);
    LinePD[2] = IM_ClampToByte((New * (LinePS[2] + Old) / Old + LinePS[2] - Old) >> 1);

  注意到这里是以24位图像为例的,其实24位图像在进行SSE优化时有的时候比32位麻烦很多,因为32位一个像素4个字节,一个SSE变量正好能容纳4个像素,而24位一个像素3个字节,很多时候要在编程时把他补充一个alpha,然后处理玩后在把这个alpha去掉。

  对于本例,注意到还有特殊性,在处理一个像素时还涉及到对应的Y分量的读取,所以有增加了复杂性。

  我们在看上下上面的公式,由于SSE没有整数除法指令,通常情况下要进行整除必须借助浮点版本的除法,因此必须有这种数据类型的转换,另外,我们考虑把括号里的加法展开下,可以得到公式变为如下:

 LinePD[0] = IM_ClampToByte((New * LinePS[0] / Old + LinePS[0] + New - Old) >> 1);

  这样展开从C的角度来说不会产生什么大的性能差异,但是对于SSE编程却有好处,注意到New和LinePS[0] 的最大只都不会超过255,因此两者相乘也在ushort所能表达的范围内,但是如果带上原来的(LinePS[0] + Old) 则会超出ushort范围,对于没有超出USHORT类型的乘法,我们可以借助_mm_mullo_epi16一次性实现8个数据的乘法,然后在根据需要把他们扩展位32位。

  具体的优化细节还有很多值得探讨的,由于之前的很多系列文章里基本已经讲到部分优化技巧,因此本文仅仅贴出最后这一块的优化代码,具体细节有兴趣的朋友可以自行去研究:

     __m128i SrcV = _mm_loadu_epi96((__m128i *)LinePS);
        __m128i OldV = _mm_cvtsi32_si128(*(int *)LinePO);
        __m128i NewV = _mm_cvtsi32_si128(*(int *)LinePN);

        __m128i SrcV08 = _mm_unpacklo_epi8(SrcV, Zero);
        __m128i OldV08 = _mm_shuffle_epi8(OldV, _mm_setr_epi8(0, -1, 0, -1, 0, -1, 1, -1, 1, -1, 1, -1, 2, -1, 2, -1));
        __m128i NewV08 = _mm_shuffle_epi8(NewV, _mm_setr_epi8(0, -1, 0, -1, 0, -1, 1, -1, 1, -1, 1, -1, 2, -1, 2, -1));
        __m128i Temp08 = _mm_sub_epi16(_mm_add_epi16(SrcV08, NewV08), OldV08);
        __m128i Mul08 = _mm_mullo_epi16(SrcV08, NewV08);
        __m128i Value04 = _mm_div_epi32(_mm_unpacklo_epi16(Mul08, Zero), _mm_unpacklo_epi16(OldV08, Zero));
        __m128i Value48 = _mm_div_epi32(_mm_unpackhi_epi16(Mul08, Zero), _mm_unpackhi_epi16(OldV08, Zero));
        __m128i Value08 = _mm_srli_epi16(_mm_add_epi16(_mm_packus_epi32(Value04, Value48), Temp08), 1);

        __m128i SrcV12 = _mm_unpackhi_epi8(SrcV, Zero);
        __m128i OldV12 = _mm_shuffle_epi8(OldV, _mm_setr_epi8(2, -1, 3, -1, 3, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1));
        __m128i NewV12 = _mm_shuffle_epi8(NewV, _mm_setr_epi8(2, -1, 3, -1, 3, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1));
        __m128i Temp12 = _mm_sub_epi16(_mm_add_epi16(SrcV12, NewV12), OldV12);
        __m128i Mul12 = _mm_mullo_epi16(SrcV12, NewV12);
        __m128i Value12 = _mm_div_epi32(_mm_unpacklo_epi16(Mul12, Zero), _mm_unpacklo_epi16(OldV12, Zero));
        __m128i Value16 = _mm_srli_epi16(_mm_add_epi16(_mm_packus_epi32(Value12, Zero), Temp12), 1);
        _mm_storeu_epi96((__m128i*)LinePD, _mm_packus_epi16(Value08, Value16));

  这里充分运用的shuffle指令来实现各种需求。

  优化后速度可以提升到7ms左右。

    本文最后的运行效果可下载测试:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar

  位于菜单Enhance –> LocalExponentialCorrection下。

SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。




 

 

转载于:https://www.cnblogs.com/Imageshop/p/9129162.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/101629.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 随机森林算法原理简要总结怎么写_旋转森林算法

    随机森林算法原理简要总结怎么写_旋转森林算法①RandomForest随机森林算法原理:即bagging法+CART算法生成决策树的结合。RF=bagging+fully-grownCARTdecisiontree②bagging法的核心:bootstrap在原始数据集D中选择若干个子数据集Dt,将子数据集单个单个进行决策树生成。③随机森林的优点:可并行化计算(子集的训练相互独立),效率高继承了CART算法的优点(使用Gini系数选择最优特征及切分点)减小了完全生成树的弊端(因为完全生成树过于复杂,Ein小但E

    2022年10月27日
  • Activexobject_javascript打开新窗口

    Activexobject_javascript打开新窗口一、功能实现核心:FileSystemObject对象    要在javascript中实现文件操作功能,主要就是依靠FileSystemobject对象。二、FileSystemObject编程 使用FileSystemObject对象进行编程很简单,一般要经过如下的步骤:创建FileSystemObject对象、应用相关方法、访问对象相关属性。 (一)创建Fi

    2022年10月14日
  • JavaScript学习第五天笔记(作用域)

    JavaScript学习第五天笔记(作用域)JavaScript学习第五天笔记(作用域)

  • 使用NodeJS实现JWT原理「建议收藏」

    使用NodeJS实现JWT原理「建议收藏」使用NodeJS实现JWT原理jwt是jsonwebtoken的简称,本文介绍它的原理,最后后端用nodejs自己实现如何为客户端生成令牌token和校验token为什么需要会话管理我们用nodejs为前端或者其他服务提供resful接口时,http协议他是一个无状态的协议,有时候我们需要根据这个请求的上下获取具体的用户是否有权限,针对用户的上下文进行操作。所以出现了cookiessession还有jwt这几种技术的出现,都是对HTTP协议的一个补充。使得我们可以用HTTP协议+状态管理构

    2022年10月17日
  • 基于Unity的AOP的符合基于角色的访问控制(RBAC)模型的通用权限设计

    基于Unity的AOP的符合基于角色的访问控制(RBAC)模型的通用权限设计

  • MyBatis 二级缓存机制[通俗易懂]

    MyBatis 二级缓存机制[通俗易懂]MyBatis 二级缓存机制

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号