SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。
这是一篇2010年比较古老的文章了,是在QQ群里一位群友提到的,无聊下载看了下,其实也没有啥高深的理论,抽空实现了下,虽然不高大上,还是花了点时间和心思优化了代码,既然这样,就顺便分享下优化的思路和经历。
文章的名字为:Contrast image correction method,由于本人博客的后台文件已经快超过博客园所容许的最大空间,这里就不直接上传文章了,大家可以直接点我提供的链接下载。
文章的核心就是对普通的伽马校正做改进和扩展,一般来说,伽马校正具有以下的标准形式:

其中I(i,j)为输入图像,O(i,j)为输出图像,γ为控制参数,当γ大于1时,图像整体变亮,当γ小于1大于0时,图像整体变暗,γ小于0算法无意义。
这个算法对于图像整体偏暗或整体偏亮时,通过调节参数γ可以获得较为满意的效果,但是如果图像中同时存在欠曝或过曝的区域,同一个参数就无法同时满意的效果了,因此,可引入一种γ随图像局部区域信息变化的算法来获取更为满意的效果,一种常用的形式如下:

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

其中的mask获取方式为:先对原图进行反色处理,然后进行一定半径的高斯模糊。
这样做的道理如下:如果mask的值大于128,说明那个点是个暗像素同时周边也是暗像素,因此γ值需要小于0以便将其增亮,mask值小于128,对应的说明当前点是个较亮的像素,且周边像素也较亮,mask值为128则不产生任何变化,同时,mask值离128越远,校正的量就越大,并且还有个特点就是纯白色和纯黑色不会有任何变化(这其实也是会产生问题的)。
如下图所示,直观的反应了不同的mask值的映射结果。

简单写一段测试代码,看看这个的效果如何:
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, ); // Blur
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
unsigned char *LinePM = Mask + Y * Stride; for (int X = ; X < Width; X++)
{
LinePD[] = IM_ClampToByte( * pow(LinePS[] * IM_INV255, pow(, ( - LinePM[]) / 128.0f))); // Moroney论文的公式
LinePD[] = IM_ClampToByte( * pow(LinePS[] * IM_INV255, pow(, ( - LinePM[]) / 128.0f)));
LinePD[] = IM_ClampToByte( * pow(LinePS[] * IM_INV255, pow(, ( - LinePM[]) / 128.0f)));
LinePS += ; LinePD += ; LinePM += ;
}
}
free(Mask);
return IM_STATUS_OK;
}
基本按照论文的公式写的代码,未做优化,测试两张图片看看。

原图1 Moroney论文的结果
似乎效果还不错。
作为一种改进,Contrast image correction method一文作者对上述公式进行了2个方面的调整,如下所示:

第一,高斯模糊的mask使用双边滤波来代替,因为双边滤波的保边特性,这样可以减少处理后的halo瑕疵。这没啥好说的。
第二,常数2使用变量α代替,并且是和图像内容相关的,具体算式如下:
当图像的整体平均值小于128时,使用
计算,当平均值大于128时,使用
计算,论文作者给出了这样做的理由:对于低对比度的图像,应该需要较强烈的校正,因此α值应该偏大,而对于有较好对比度的图,α值应该偏向于1,从而产生很少的校正量。
对于第二条,实际上存在很大的问题,比如对于我们上面进行测试的原图1,由于他上半部分为天空,下半部分比较暗,且基本各占一般,因此其平均值非常靠近128,因此计算出的α也非常接近1,这样如果按照改进后的算法进行处理,则基本上图像无什么变化,显然这是不符合实际的需求的,因此,个人认为作者这一改进是不合理的,还不如对所有的图像该值都取2,靠mask值来修正对比度。
那么对于彩色图像,我们有两种方法,一种是直接对RGB各分量处理,如上面的代码所示,另外一种就是把他转换到YCBCR或者LAB或者YUV等空间,然后只处理亮度通道,最后在转换到RGB空间,那么本文对我的有用的帮助就是提供了一个恢复色彩饱和度的方法。一般来说在对Y分量做处理后,再转换到RGB空间,图像会出现饱和度一定程度丢失的现象,看上去图像似乎色彩不足。如下图中间图所示,因此,论文提出了下面的修正公式:

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

原图 直接处理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, ), , 0.01f); // 通过Y通道数据处理得到255-Mask值
unsigned char *NewY = Mask;
for (int Y = ; Y < Height * Width; Y++)
{
NewY[Y] = IM_ClampToByte( * pow(OldY[Y] * IM_INV255, pow(, ( - ( - Mask[Y])) / 128.0f)));
} for (int Y = ; 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 = ; X < Width; X++, LinePS += , LinePD += , LinePO++, LinePN++)
{
int Old = LinePO[], New = LinePN[];
if (Old == )
{
LinePD[] = ; LinePD[] = ; LinePD[] = ;
}
else
{
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
}
}
}
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( * * sizeof(unsigned char));
for (int Y = ; Y < ; Y++)
{
float Gamma = pow(, ( - ( - Y)) / 128.0f);
for (int X = ; X < ; X++)
{
Table[Y * + X] = IM_ClampToByte( * pow(X * IM_INV255, Gamma));
}
} for (int Y = ; Y < Height * Width; Y++)
{
NewY[Y] = Table[Mask[Y] * + OldY[Y]];
}
free(Table);
速度一下子跳到了15ms,由于是查表,基本上无SSE优化的发挥地方。
接着再看最后的饱和度校正部分的算法,核心代码即:
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
LinePD[] = IM_ClampToByte((New * (LinePS[] + Old) / Old + LinePS[] - Old) >> );
注意到这里是以24位图像为例的,其实24位图像在进行SSE优化时有的时候比32位麻烦很多,因为32位一个像素4个字节,一个SSE变量正好能容纳4个像素,而24位一个像素3个字节,很多时候要在编程时把他补充一个alpha,然后处理玩后在把这个alpha去掉。
对于本例,注意到还有特殊性,在处理一个像素时还涉及到对应的Y分量的读取,所以有增加了复杂性。
我们在看上下上面的公式,由于SSE没有整数除法指令,通常情况下要进行整除必须借助浮点版本的除法,因此必须有这种数据类型的转换,另外,我们考虑把括号里的加法展开下,可以得到公式变为如下:
LinePD[] = IM_ClampToByte((New * LinePS[] / Old + LinePS[] + New - Old) >> );
这样展开从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(, -, , -, , -, , -, , -, , -, , -, , -));
__m128i NewV08 = _mm_shuffle_epi8(NewV, _mm_setr_epi8(, -, , -, , -, , -, , -, , -, , -, , -));
__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), ); __m128i SrcV12 = _mm_unpackhi_epi8(SrcV, Zero);
__m128i OldV12 = _mm_shuffle_epi8(OldV, _mm_setr_epi8(, -, , -, , -, , -, -, -, -, -, -, -, -, -));
__m128i NewV12 = _mm_shuffle_epi8(NewV, _mm_setr_epi8(, -, , -, , -, , -, -, -, -, -, -, -, -, -));
__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), );
_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优化。的更多相关文章
- SSE图像算法优化系列十六:经典USM锐化中的分支判断语句SSE实现的几种方法尝试。
分支判断的语句一般来说是不太适合进行SSE优化的,因为他会破坏代码的并行性,但是也不是所有的都是这样的,在合适的场景中运用SSE还是能对分支预测进行一定的优化的,我们这里以某一个算法的部分代码为例进行 ...
- SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。
关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...
- Web 前端开发精华文章集锦(jQuery、HTML5、CSS3)【系列十九】
<Web 前端开发精华文章推荐>2013年第七期(总第十九期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各种增强网站用户体验的 jQuery 插件,展示前沿的 HTML5 和 C ...
- “全栈2019”Java第九十九章:局部内部类与继承详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。
在很多场合需要高效率的肤色检测代码,本人常用的一个C++版本的代码如下所示: void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char ...
- SSE图像算法优化系列十二:多尺度的图像细节提升。
无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...
- SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。
在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是 ...
- SSE图像算法优化系列十八:三次卷积插值的进一步SSE优化。
本文是在学习https://blog.csdn.net/housisong/article/details/1452249一文的基础上对算法的理解和重新整理,再次非常感谢原文作者的深入分析以及分享. ...
- 学习ASP.NET Core Razor 编程系列十九——分页
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
随机推荐
- BZOJ3230 相似子串 字符串 SA ST表
原文链接http://www.cnblogs.com/zhouzhendong/p/9033092.html 题目传送门 - BZOJ3230 题意 给定字符串$s$.长度为$n$. 现在有$Q$组询 ...
- 删除Docker中所有已停止的容器
方法一: #显示所有的容器,过滤出Exited状态的容器,取出这些容器的ID, sudo docker ps -a|grep Exited|awk '{print $1}' #查询所有的容器,过滤出E ...
- mysql的基础增删改查(一)
修改,操作表:1.建表:create table MyClass(id int(4) not null primary key auto_increment,name char(20) not nul ...
- Windows10家庭版如何升级至Windows10专业版
Windows10家庭版和专业版系统文件其实是一样的iso镜像文件,但是由于Microsoft某些限制导致一些用户无法享受到专业版的福利,说实话这是一种很让人蛋疼的操作... 接下来我来告诉各位如何把 ...
- 分布式事务?咱先弄明白本地事务再说 - ACID
过去一段时间面试的同学,对于数据库事务,可以按照配置正常使用,但很多都无法讲清楚和理解数据库事务这个东西真正的意义,以及互联网兴起以后,当今数据库在ACID面前面临怎样的问题和抉择. 事务,是各大 ...
- JVM 调优-给你的java应用看看病
目录 java 应用 1 cpu 负载过高 1.1 分析问题 1.2 解决方案 2 内存占用过多 2.1 从内存回收方面 2.2 从代码层面 java 应用 1 cpu 负载过高 1.1 分析问题 首 ...
- SparkException: Could not find CoarseGrainedScheduler or it has been stopped.
org.apache.spark.SparkException: Could not find CoarseGrainedScheduler or it has been stopped. at or ...
- XamarinSQLite教程创建数据库
XamarinSQLite教程创建数据库 安装SQLite/SQL Server Compact Toolbox后,就可以使用该插件创建数据库了.操作步骤如下: (1)在Visual Studio中, ...
- re模块、hashlib模块
一.re模块 1.什么是正则? 正则就是用一系列具有特殊含义的字符组成一套规则,该规则用来描述具有某一特征的字符串,正则就是用来去一个大的字符串中匹配出符合规则的子字符串 2.为何要用正则? 用户注册 ...
- BZOJ.5287.[AHOI HNOI2018]毒瘤(虚树 树形DP)
BZOJ LOJ 洛谷 设\(f[i][0/1]\)表示到第\(i\)个点,不选/选这个点的方案数.对于一棵树,有:\[f[x][0]=\prod_{v\in son[x]}(f[v][0]+f[v] ...