SSE图像算法优化系列三十一:RGB2HSL/RGB2HSV及HSL2RGB/HSV2RGB的指令集优化-上。
RGB和HSL/HSV颜色空间的相互转换在我们的图像处理中是有着非常广泛的应用的,无论是是图像调节,还是做一些肤色算法,HSL/HSV颜色空间都非常有用,他提供了RGB颜色空间不具有的一些独特的特性,但是由于HSL/HSV颜色空间的复杂性,他们之间的转换的效率一直不是很高的,有一些基于定点算法的尝试,对速度有一定的提升,但一个是提升不是特别的明显,另外就是对结果的精度有一定的影响。
对于这两个算法的指令集优化,网络上就根本没有任何资料,也没有任何人进行过尝试,我也曾经有想法去折腾他,但是初步判断觉得他里面有太多的分支了,应该用了指令集后也不会有多大的速度区别,所以一直没有动手。
但是最近的一个朋友的潜在需求,然后我又对这个算法有些期待,重新动手拾起这个转换过程,结果还是有所收获,速度获得了3到4倍的提升。、
我们先来谈谈RGB到HSL或者HSV颜色空间的转换优化
这个网络上一大堆,我也就不浪费时间去重新整理,我直接分享一段代码和网址吧:
参考网址: http://www.xbeat.net/vbspeed
这个文章给出的是VB6的代码,可以参考下。
我们约定:RGB数据源是unsigned char 类型, 有效范围就是[0,255],而HSL/HSV都是浮点型,其中H的有效范围时[0,6],S的有效范围是[0,1], L/V的有效范围也是[0,1]。
经过我个人的整理和稍微优化,一个简单的RGB2HSV代码如下所示:
void IM_RGB2HSV_PureC(unsigned char Blue, unsigned char Green, unsigned char Red, float &Hue, float &Sat, float &Val)
{
int Min = IM_Min(Red, IM_Min(Green, Blue));
int Max = IM_Max(Red, IM_Max(Green, Blue));
if (Max == Min)
{
Hue = 0;
Sat = 0;
}
else
{
int Delta = Max - Min;
if (Max == Red)
Hue = (float)(Green - Blue) / Delta;
else if (Max == Green)
Hue = 2.0f + (float)(Blue - Red) / Delta;
else
Hue = 4.0f + (float)(Red - Green) / Delta; // 实际上只有Max==Red时,方有可能Hue < 0 (对应Green < Blue),
// 所以有的代码在Max == Red内再做判断,对于C代码来说,这样效率应该会高一点
if (Hue < 0) Hue += 6;
Sat = (float)Delta / Max;
}
Val = Max * IM_Inv255;
}
RGB2HSL的代码如下:
void IM_RGB2HSL_PureC(unsigned char Blue, unsigned char Green, unsigned char Red, float &Hue, float &Sat, float &Val)
{
int Min = IM_Min(Red, IM_Min(Green, Blue));
int Max = IM_Max(Red, IM_Max(Green, Blue));
int Sum = Max + Min;
if (Max == Min)
{
Hue = 0;
Sat = 0;
}
else
{
int Delta = Max - Min;
if (Max == Red)
Hue = (float)(Green - Blue) / Delta;
else if (Max == Green)
Hue = 2.0f + (float)(Blue - Red) / Delta;
else
Hue = 4.0f + (float)(Red - Green) / Delta; if (Hue < 0) Hue += 6; if (Sum <= 255)
Sat = (float)Delta / Sum;
else
Sat = (float)Delta / (510 - Sum);
}
Val = Sum * IM_Inv510;
}
比较两个不同的模型的代码可以发现,他们对于H分量的定义是相同的,对于V/L分量一个使用了最大值,一个使用了最大值和最小值的平均值,对于S分量,大家都考虑了最大值和最下值的差异,只是一个和最大值做比较,一个是和最大值和最小值之和做比较,整体来说,RGB2HSV模型相对来说简单一些,计算量也少一些。
可以看到,无论是RGB2HSL还是RGB2HSV,求H的过程都有非常多的判断和分支语句,而且整体考虑除零错误(Max == Min)还有一些其他的特殊判断, 正如我在博文中多次提到,指令集里没有分支跳转的东西,这些跳转是非常不利于指令集优化。指令集里要实现这样的东西,只有两个办法:
1、想办法把所有分支跳转用一些奇技淫巧合并到一起,用一个语句来表达他。
2、对所有分支语句的结果都计算出来,然后使用相关的Blend进行条件合并。
仔细的分析上面的C代码,我是没有想到什么特别好的技巧把色相部分的三个分支合并为一个语句。凭个人的感觉,只能使用第二种方式。
为了描述方便,我先贴出RGB2HSV算法一个比较简单的SIMD指令集优化的结果:
1 void IM_RGB2HSV_SSE_Old(__m128i Blue, __m128i Green, __m128i Red, __m128 &Hue, __m128 &Sat, __m128 &Val)
2 {
3 __m128i Max = _mm_max_epi32(Red, _mm_max_epi32(Green, Blue)); // R/G/B的最大值Max
4 __m128i Min = _mm_min_epi32(Red, _mm_min_epi32(Green, Blue)); // R/G/B的最小值Min
5 __m128i Delta = _mm_sub_epi32(Max, Min); // 最大值和最小值之间的差异Delta = Max - Min
6
7 __m128 MaxS = _mm_cvtepi32_ps(Max);
8 __m128 DeltaS = _mm_cvtepi32_ps(Delta);
9
10 Sat = _mm_divz_ps(DeltaS, MaxS); // S = Delta / Max, 注意有了除零的异常处理,同时如果Max == Min, Delta就为0, S也返回0,是正确的
11 Val = _mm_mul_ps(MaxS, _mm_set1_ps(IM_Inv255)); // V = Max / 255;
12
13 __m128 Inv = _mm_divz_ps(_mm_set1_ps(1), DeltaS);
14
15 //if (Max == Red)
16 // Hue = (float)(Green - Blue) / Delta;
17
18 __m128 HueR = _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Green, Blue)), Inv);
19
20 //else if (Max == Green)
21 // Hue = 2.0f + (float)(Blue - Red) / Delta;
22
23 __m128i Mask = _mm_cmpeq_epi32(Max, Green);
24 __m128 HueG = _mm_add_ps(_mm_set1_ps(2.0f), _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Blue, Red)), Inv));
25
26 Hue = _mm_castsi128_ps(_mm_blendv_epi8(_mm_castps_si128(HueR), _mm_castps_si128(HueG), Mask));
27
28 //else
29 // Hue = 4.0f + (float)(Red - Green) / Delta;
30 Mask = _mm_cmpeq_epi32(Max, Blue);
31 __m128 HueB = _mm_add_ps(_mm_set1_ps(4.0f), _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Red, Green)), Inv));
32
33 Hue = _mm_castsi128_ps(_mm_blendv_epi8(_mm_castps_si128(Hue), _mm_castps_si128(HueB), Mask));
34
35 // if (H < 0) H += 6; 其实这个主要是针对Max == R的情况会出现
36 Hue = _mm_blendv_ps(Hue, _mm_add_ps(Hue, _mm_set1_ps(6)), _mm_cmplt_ps(Hue, _mm_setzero_ps()));
37
38 }
说明: IM_RGB2HSV_SSE函数中的Blue、Green、Red三个__m128i 变量中保存的是4个32位的颜色分量,而不是16个颜色。
第三、第四、第五行求Max\Min\Delta这些过程没有什么难以理解的。第七、第八行只是把整形转换为浮点型(注意SSE指令也是强类型的哦,必须自己手动转换类型)。
第十、第十一行直接就求出了Sat和Val分量, Val不难理解,Sat在对应的C代码中是分了Max == Min及Max != Min两种状况,当Max == Min时,为0,否则,要使用(Max - Min) / Max, 其实这里不用做判断直接统一使用 (Max - Min) / Max即可,因为Max == Min时, Max - Min也是0, 但是唯一需要注意的就是如果Max = Min = 0时, Max也为0, 0 / 0 在数学时不容许的,在计算上也会有溢出错误,所以这里使用了一个自定义的_mm_divz_ps函数,实现当除数为0时,返回0的结果。这样就可以剥离掉这个分支语句了。
复杂的是Hue分量的计算,从第十三行开始一直到最后都是关于他的优化。
第13行,我们先计算出1.0f / Delta,注意这里也是使用的_mm_divz_ps函数。
第16行我们先按照公式计算出当Max == Red时Hue的结果。
第23行我们比较Max和Green是否相等,注意这里也是使用的32位int类型的比较。
第24行按照公式计算出当Max == Green时,Hue分量的结果。
第25行则对这两个结果进行混合,这里的混合有很多编码上的技巧,因为我们两次计算的HueR和HueG都是__m128类型,而我们的比较是用的整形的比较,返回的是__m128i型的数据,而_mm_blendv_ps的混合需要的__m128的比较结果,但是如果直接将Mask强制转换为浮点类型,作为_mm_blendv_ps的参数,将会产生不正确的结果。那么解决方案有2个:
一、使用浮点类型的比较,即将Blue\Green分量先转换为__m128型,然后使用_mm_cmpeq_ps进行比较,这样增加几条类型转换函数。
二、就是使用本例的代码,使用_mm_blendv_epi8 + _mm_castps_si128进行混合,表面上看多了3次cast的过程,似乎更为耗时,但是实际上cast系列的语句只是个语法糖,编译后他不产生任何汇编指令。他只是让编译器认为他是另外一个类型的数据类型了,这样就可以编译了,实际上__m128、__m128i这些东西在硬件上都是保存在XMM寄存器上的,寄存器本身不分数据类型。
第30和31行也是类似的到里,对那些Max == Blue分量的结果进行混合。
第36行则是对Hue < 0的特殊情况进行处理。也没有什么特别复杂的。
我们对一副5000*5000大小的24位图像(填充的随机数据)进行测试,普通C语言的耗时约为114ms,上述SIMD优化的耗时约为 49ms,提速比接近2.2倍。
实际上上述SIMD指令优化的代码还有一定的优化空间,我们注意到为了计算HueR\HueG\HueB,我们进行了3次浮点版本的乘法和加法。但是如果我们把这个乘法和加法的部分单独提出来,每次都进行相应的混合,那么只需要最后进行一次乘法和加法即可以了,这样增加了混合的次数,但是减少了计算的次数,而混合指令其实都是通过位运算实现的,相对来说非常快,具体的代码如下所示:
1 void IM_RGB2HSV_SSE(__m128i Blue, __m128i Green, __m128i Red, __m128 &Hue, __m128 &Sat, __m128 &Val)
2 {
3 __m128i Max = _mm_max_epi32(Red, _mm_max_epi32(Green, Blue)); // R/G/B的最大值Max
4 __m128i Min = _mm_min_epi32(Red, _mm_min_epi32(Green, Blue)); // R/G/B的最小值Min
5 __m128i Delta = _mm_sub_epi32(Max, Min); // 最大值和最小值之间的差异Delta = Max - Min
6
7 __m128 MaxS = _mm_cvtepi32_ps(Max);
8 __m128 DeltaS = _mm_cvtepi32_ps(Delta);
9
10 Sat = _mm_divz_ps(DeltaS, MaxS); // S = Delta / Max, 注意有了除零的异常处理,同时如果Max == Min, Delta就为0, S也返回0,是正确的
11 Val = _mm_mul_ps(MaxS, _mm_set1_ps(IM_Inv255)); // V = Max / 255;
12
13 // SIMD没有跳转方面的指令,只能用Blend加条件判断来实现多条件语句。注意观察三种判断的情况可以看成是一个Base(0/120/240)加上不同的Diff乘以Inv。
14 // 以Max == B为基础,这样做的好处是:当Max == Min时,H是要返回0的,但是如果按照C语言的那个混合顺序,则最后判断Max == B时成立,则H返回的是4,那么为了返回正确的结果
15 // 就还要多一个_mm_blendv_epi8语句,注意这里隐藏的一个事实是Max == Min时,G - B, B - R, R - G其实都是为0的,那么类似这样的 (float)(G - B) / Delta * 60结果也必然是0。
16
17 // if (Max == bB)
18 // H = 4.0f + (float)(R - G) / Delta;
19
20 __m128i Base = _mm_set1_epi32(4);
21 __m128i Diff = _mm_sub_epi32(Red, Green);
22
23 //if (Max == G)
24 // H = 2.0f + (float)(B - R) / Delta;
25
26 __m128i Mask = _mm_cmpeq_epi32(Max, Green);
27 Base = _mm_blendv_epi8(Base, _mm_set1_epi32(2), Mask); // 当Mask为真时,_mm_blendv_epi8返回第二个参数的值,否则返回第一个参数的值
28 Diff = _mm_blendv_epi8(Diff, _mm_sub_epi32(Blue, Red), Mask);
29
30 // if (Max == R)
31 // H = (float)(G - B) / Delta;
32 Mask = _mm_cmpeq_epi32(Max, Red);
33 Base = _mm_blendv_epi8(Base, _mm_setzero_si128(), Mask);
34 Diff = _mm_blendv_epi8(Diff, _mm_sub_epi32(Green, Blue), Mask);
35
36 __m128 Inv = _mm_divz_ps(_mm_set1_ps(1), DeltaS); // 1 / Delta,注意有了除零的异常处理
37
38 // H = Base + Diff * Inv
39 Hue = _mm_add_ps(_mm_cvtepi32_ps(Base), _mm_mul_ps(_mm_cvtepi32_ps(Diff), Inv));
40
41 // if (H < 0) H += 6; 其实这个主要是针对Max == R的情况会出现
42 Hue = _mm_blendv_ps(Hue, _mm_add_ps(Hue, _mm_set1_ps(6)), _mm_cmplt_ps(Hue, _mm_setzero_ps()));
43
44 }
通过这种方式优化大概还能获取15-25%的性能提升。
当然,这里可能还有一部分空间可以考虑,即我们使用的是32位int类型的比较,一次只能比较4个数,另外诸如_mm_max_epi32这样的计算,对于原始的图像数据来说,都可以使用epi8来做的,这样一次性就是可以获取16个像素的信息,而不是8位,但是这样做面临的问题就是后面要做多次数据类型转换。这些转换的耗时和比较的耗时孰重孰轻暂时还没有结论,有兴趣的读者可以自行测试下。
如果您看懂了RGB2HSV的SSE代码,那么RGB2HSL你觉得还会有难度吗,希望读者可以自行编码实现。
下一篇将着重讲述HSL2RGB及HSV2RGB空间的优化,那个的优化难度以及优化的提速比相对来讲要比RGB2HSL和RGB2HSL更为复杂和有效。
本文的测试代码可从下述链接获取: https://files.cnblogs.com/files/Imageshop/RGB2HSV.rar?t=1689216617&download=true
如果想时刻关注本人的最新文章,也可关注公众号或者添加本人微信: laviewpbt

SSE图像算法优化系列三十一:RGB2HSL/RGB2HSV及HSL2RGB/HSV2RGB的指令集优化-上。的更多相关文章
- SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化。
一.基础原理 Base64是一种用64个Ascii字符来表示任意二进制数据的方法.主要用于将不可打印的字符转换成可打印字符,或者简单的说是将二进制数据编码成Ascii字符.Base64也是网络 ...
- SSE图像算法优化系列三十:GIMP中的Noise Reduction算法原理及快速实现。
GIMP源代码链接:https://gitlab.gnome.org/GNOME/gimp/-/archive/master/gimp-master.zip GEGL相关代码链接:https://gi ...
- SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化
二值图像的细化算法也有很多种,比较有名的比如Hilditch细化.Rosenfeld细化.基于索引表的细化.还有Opencv自带的THINNING_ZHANGSUEN.THINNING_GUOHALL ...
- SSE图像算法优化系列三:超高速导向滤波实现过程纪要(欢迎挑战)
自从何凯明提出导向滤波后,因为其算法的简单性和有效性,该算法得到了广泛的应用,以至于新版的matlab都将其作为标准自带的函数之一了,利用他可以解决的所有的保边滤波器的能解决的问题,比如细节增强.HD ...
- SSE图像算法优化系列二十一:基于DCT变换图像去噪算法的进一步优化(100W像素30ms)。
在优化IPOL网站中基于DCT(离散余弦变换)的图像去噪算法(附源代码) 一文中,我们曾经优化过基于DCT变换的图像去噪算法,在那文所提供的Demo中,处理一副1000*1000左右的灰度噪音图像耗时 ...
- 性能优化系列三:JVM优化
一.几个基本概念 GCRoots对象都有哪些 所有正在运行的线程的栈上的引用变量.所有的全局变量.所有ClassLoader... 1.System Class.2.JNI Local3.JNI Gl ...
- ElasticSearch优化系列三:机器设置(内存)
heap参数设置优化 命令行修改 ./bin/elasticsearch -Xmx10g -Xms10g xmx-JVM最大允许分配的堆内存,按需分配 xms-JVM初始分配的堆内存 此值设置与-Xm ...
- BizTalk开发系列(三十一)配置和使用HTTP适配器
BizTalk的主机分别进程内主机和独立主机.但由于一直使用的是进程内主机,对于独立主机的认识比较模糊,前不久在做一个BizTalk的项目的时 候,个别系统使用HTTP的方式发布Txt之类的文本的.刚 ...
- Mysql优化系列(1)--Innodb引擎下mysql自身配置优化
1.简单介绍InnoDB给MySQL提供了具有提交,回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎.InnoDB锁定在行级并且也在SELECT语句提供一个Oracle风格一致的非锁定读.这些特色 ...
- 推荐:Java性能优化系列集锦
Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难.随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了.现代JVM持续演 ...
随机推荐
- Mysql8.0为什么取消了缓存查询的功能
首先我们介绍一下MySQL的缓存机制 [MySQL缓存机制]简单的说就是缓存sql文本及查询结果,如果运行完全相同的SQL,服务器直接从缓存中取到结果,而不需要再去解析和执行SQL. 但如果表中任何数 ...
- 深入理解python虚拟机:程序执行的载体——栈帧
深入理解python虚拟机:程序执行的载体--栈帧 栈帧(Stack Frame)是 Python 虚拟机中程序执行的载体之一,也是 Python 中的一种执行上下文.每当 Python 执行一个函数 ...
- react项目使用less样式,配置less
create-react-app脚手架创建的项目有sass配置项,使用的时候只需要装包即可,下面是less使用的方法 由于 create-react-app 脚手架中并没有配置关于 less 文件的解 ...
- [Pytorch框架] 3.1 logistic回归实战
文章目录 3.1 logistic回归实战 3.1.1 logistic回归介绍 3.1.2 UCI German Credit 数据集 3.2 代码实战 import torch import to ...
- java封装和关键字
一.封装 封装:告诉我们如何正确设计对象的属性和方法 对象代表什么,就得封装对应的数据,并提供数据对应的行为 封装的好处: 让编程变得很简单,有什么事,找对象,调方法 降低学习成本,可以少学,少记,或 ...
- 代码随想录算法训练营Day44 动态规划
代码随想录算法训练营 代码随想录算法训练营Day44 动态规划|完全背包 518. 零钱兑换 II 377. 组合总和 Ⅳ 完全背包 有N件物品和一个最多能背重量为W的背包.第i件物品的重量是weig ...
- MVCC-数据库
参考地址:看一遍就理解:MVCC原理详解 - 掘金 (juejin.cn) 1. 相关数据库知识点回顾 1.1 什么是数据库事务,为什么要有事务 事务,由一个有限的数据库操作序列构成,这些操作要么全部 ...
- 驱动开发:内核解析PE结构导出表
在笔者的上一篇文章<驱动开发:内核特征码扫描PE代码段>中LyShark带大家通过封装好的LySharkToolsUtilKernelBase函数实现了动态获取内核模块基址,并通过ntim ...
- 利用APIFOX对ABAP函数进行调用
1.安装APIFOX,当然也可以使用在线版,无需下载 官网地址:https://apifox.com/ 2.新建项目 3.为项目起一个名称,为相关开发测试人员授权 4.在根目录新增子目录 5.编辑开发 ...
- TLS详解(原理和实践)
主页 个人微信公众号:密码应用技术实战 个人博客园首页:https://www.cnblogs.com/informatics/ 引言 本文主要内容涉及到TLS协议发展历程.TLS协议原理以及在HTT ...