SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法。
因未测试其他作者的算法时间和效率,本文不敢自称是最快的,但是速度也可以肯定说是相当快的,在一台I5机器上占用单核的资源处理 3000 * 2000的灰度数据用时约 20ms,并且算法和核心的大小是无关的,即所谓的o(1)算法。
在实现本算法之前,也曾经参考何凯明在暗通道去雾时提出的一篇参考论文中的算法: STREAMING MAXIMUM-MINIMUM FILTER USING NO MORE THAN THREE COMPARISONS PER ELEMENT ,这篇文章所描述的过程也是o(1)的,速度也相当快,不过还是不够快。
我曾经自己构思了一个想法,也是基于行列分离的,在速度上比上文的代码又要快,并且也是o(1)算法,但是算法速度和图片的内容有关,比如对一个图进行了一次算法后,再次对结果执行相同的算法,可能后一次就要慢很多,这和我改进的算法本身有关系,但是这种情况是很少见的。
本文所要介绍的算法也是在很久以前就看到过的,但是一直没有引起我的重视,其对应的参考论文是 A fast algorithm for local minimum and maximum filters on rectangular and octagonal kernels ,当时觉得论文的实现起来可能没有我自己构思的快,也就没有去深究。
论文的实现步骤也是基于行列分离的,即先进行行方向的一维运算,然后再对结果进行列方向的一维计算,具体的理论描述大家去研究论文吧。
那其实论文的核心就是下面这个图。
In表示一维行方向的输入数据,g/h分别运算过程中的两个中间数据,和输入数据大小一样。
如上图所示,我们假定需要进行计算的核大小为R,那么将一行分为多个大小为 D =(2R+1) 的分段,例如图中R=2, D=5 ,对每一个分段进行预处理,其中 x 号位置存放的是箭头所在直线段上的点中的最大值(最小值),如此处理得到 g 和 h 两个数组,那么对于某个点(索引为I),其半径R内的最大(小)值为:Max/ Min(g(I+R),h(I-R))。
过程好简单。
我们拿一组数据来说明上述过程,假如一行数据如下,我们要进行膨胀操作(最大值),核半径为2:
In: 20 12 35 9 10 7 32 15 20 45 28
对应的g/h为:
g: 20 20 35 35 35 7 32 32 32 45 45
h: 35 35 35 10 9 45 45 45 45 45 28
如果我们要计算第4个点的半径为2的最大值,则对应 g(I+R) = g(4+2) = 7, h(I-R)=h(4-2)=35, 得到结果为max(7,35) = 35;
如果我们要计算第6个点的半径为2的最大值,则对应 g(I+R) = g(6+2) = 32, h(I-R)=h(6-2)=10, 得到结果为max(32,10) = 32;
注意上面索引是以1位小标计数起点的。
边缘处理:
注意到在边缘处,比如左侧边缘,当要计算的点的索引小于R时,这时h值是无效的,而在右侧边缘,g值是无法取到的,但是仔细观察,问题很简单,还拿上述数据为例,如果要计算索引为2处半径为2的最值,由于h(2-2)是超出索引的(前面说了本例以1为下标起点),因此不能用前述的方式,那我们回归到问题的本质,计算2处半径为2的最值,就是计算max(In(1), In(2), In(3), In(4)), 而这个值不就是g数据中索引为2+2处的数据吗,在这之前就已经帮我们算法,直接取值就可以了。
在数据尾部(右侧),情况有所不同,我们应该从H中取值。
从上述的分析可知,这个算法有个特性,就是半径越大,算法的耗时会越小,因为对边缘的处理只需要拷贝数据,而没有了更多的计算,可能很多人不信吧。
算法实现:
有了上面的描述,要实现一个快速的腐蚀或膨胀算法相信对部分来说应该是一件非常容易的事情,先行方向处理,在列方向,好简单。
最近我是迷上了SSE算法优化,于是就思考了这个算法的SSE优化,以前在看SSE的函数时,就一直在想_mm_max_epi8/_mm_min_epi8这种一次性能获取16个字节数据的最值的函数是否能用在腐蚀和膨胀上,不过由于他们是对两个连续数据的比较,在行方向上是难以运用的,但是如果数据比较是列方向上,那不就可以使用了吗。
我们上述算法就有列方向的比较,不就有了用武之地了。
首先,我们给出在列方向更新g值/h值在每个分段范围内的C语言实现代码,比如获取g值大概的代码如下:
memcpy(G + StartY * ValidDataLength, Src + StartY * Stride, ValidDataLength);
for (int Y = StartY + ; Y < EndY; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = G + Y * ValidDataLength;
unsigned char *LinePD = G + (Y - ) * ValidDataLength;
for (int X = ; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
}
}
StartY为计算好的分段范围的起点,EndY为分段范围的终点,我们观察g数据的规律,知道在分段范围内第一行的最大值就是数据本身,而后面的则是和前一行比较得到结果。
很明显:
for (int X = ; X < Width * Channel; X++)
{
LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
}
这个代码很容易向量化,如果这里是浮点运算,编译器会直接帮我们向量处理的,但是对于字节,似乎编译器还没有那么智能,我们自己手动来向量化,代码如下:
memcpy(G + StartY * ValidDataLength, Dest + StartY * ValidDataLength, ValidDataLength); // 每段G数据第一行就是原始的数据
for (int Y = StartY + ; Y < EndY; Y++)
{
unsigned char *LinePS = Dest + Y * ValidDataLength;
unsigned char *LinePD = G + Y * ValidDataLength;
unsigned char *LinePL = G + (Y - ) * ValidDataLength;
for (int X = ; X < Block * BlockSize; X += BlockSize)
{
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
}
for (int X = Block * BlockSize; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
}
}
其中BlockSize = 16, Block = ValidDataLength / BlockSize;
这段代码很简单,对于h的处理也是类似的道理。
当我们获得了g,h的数据后,后面的处理过程的C代码也很简单:
for (int Y = Radius; Y < IM_Min(BlockV * Size - Radius, Width); Y++) // 这些位于中间的数据是符合 G+Radius 和 R - Radius 取大的要求的
{
unsigned char *LinePG = G + IM_Min(Y + Radius, Width - ) * Width;
unsigned char *LinePH = H + (Y - Radius) * ValidDataLength;
unsigned char *LinePD = T + Y * ValidDataLength;for (int X = 0; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePG[X], LinePH[X]);
}
}
又是同样的道理,内部的for循环可直接用SSE代替。
但是这里只是对列方向进行处理,行方向有没有可能用SSE做处理呢,可以肯定的说,绝对可以,但是除非你非常有耐心,以为其中各种pack和unpack或者shuffle会让你疯掉,而且最后的效率也许还不如直接使用普通的C语言。
那么如何处理呢,我想大家肯定能想到转置,确实,对数据进行转置后再进行列方向的处理,然后再转置回来就相当于对原数据的行方向处理。
关于转置,一直也是个耗时的过程,但是我在图像转置的SSE优化(支持8位、24位、32位),提速4-6倍 一文中提到了利用SSE实现高速的转置操作,利用它去实现本文的流程则非常靠谱。
那么我们贴出整体的大部分处理代码:
垂直方向的核心:
int IM_Vert_MaxFilter(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Radius)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; // 从节省内存的角度考虑,可以只需要两倍额外的内存
unsigned char *G = (unsigned char *)malloc(Height * Width * Channel * sizeof(unsigned char));
unsigned char *H = (unsigned char *)malloc(Height * Width * Channel * sizeof(unsigned char)); if ((G == NULL) || (H == NULL))
{
if (G != NULL) free(G);
if (H != NULL) free(H);
return IM_STATUS_OUTOFMEMORY;
} // 垂直方向处理
int Size = Radius * + , ValidDataLength = Width * Channel;
int BlockSize = , Block = ValidDataLength / BlockSize;
int BlockV = ((Height % Size) == ? Height / Size : (Height / Size) + ); for (int Index = ; Index < BlockV; Index++)
{
int StartY = Index * Size, EndY = IM_Min(Index * Size + Size, Height);
memcpy(G + StartY * ValidDataLength, Src + StartY * Stride, ValidDataLength); // 每段G数据第一行就是原始的数据 for (int Y = StartY + ; Y < EndY; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = G + Y * ValidDataLength;
unsigned char *LinePL = G + (Y - ) * ValidDataLength;
for (int X = ; X < Block * BlockSize; X += BlockSize)
{
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
}
for (int X = Block * BlockSize; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
}
} memcpy(H + StartY * ValidDataLength, G + (EndY - ) * ValidDataLength, ValidDataLength); // 每段H数据的第一行就是对应G数据的最后一行
memcpy(H + (EndY - ) * ValidDataLength, Src + (EndY - ) * Stride, ValidDataLength); // 每段H数据的最后一行就是原始的数据 for (int Y = EndY - ; Y > StartY; Y--) // 注意循环次数的改变
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = H + Y * ValidDataLength;
unsigned char *LinePL = H + (Y + ) * ValidDataLength;
for (int X = ; X < Block * BlockSize; X += BlockSize)
{
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
}
for (int X = Block * BlockSize; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
}
}
// 针对最值算法,在列方向最后一块不是Size大小时,后面的数据只能是重复边缘像素,这样后面跟的G/H值和Height - 1大小是相同的
} // 整个的数据分为三个部分,[0, Radius]为第一组,[Radius, BlockV * Size - Radius]为第二组,[BlockV * Size - Radius, BlockV * Size]为第三组,
// 第一组数据的结果取G中[Radius, 2 * Radius]的值,第二组数据取G + Radius和H - Radius中的小值,第三组取H - Radius的值。 // 最顶部的一半数据,此时的H数据无效
//
// 此处删除若干代码
// for (int Y = Radius; Y < IM_Min(BlockV * Size - Radius, Height); Y++) // 这些位于中间的数据是符合 G + Radius 和 R - Radius 取大的要求的
{
unsigned char *LinePG = G + IM_Min(Y + Radius, Height - ) * ValidDataLength; // 有可能超出范围的
unsigned char *LinePH = H + (Y - Radius) * ValidDataLength;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = ; X < Block * BlockSize; X += BlockSize)
{
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePG + X)), _mm_loadu_si128((__m128i *)(LinePH + X))));
}
for (int X = Block * BlockSize; X < ValidDataLength; X++)
{
LinePD[X] = IM_Max(LinePG[X], LinePH[X]);
}
}
// 最底部的一半数据,此时的G数据无用
//
// 此处删除若干代码
//
free(G);
free(H);
return IM_STATUS_OK;
}
综合的调用:
int IM_MaxFilter(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Radius)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; int Status = IM_STATUS_OK;
unsigned char *T = (unsigned char *)malloc(Height * Stride * sizeof(unsigned char));
if (T == NULL) return IM_STATUS_OUTOFMEMORY;
Status = IM_Vert_MaxFilter(Src, T, Width, Height, Stride, Radius);
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Transpose(T, Dest, Width, Height, Stride, Height, Width, Height * Channel); // 转置,注意Dest我只用了Height * Channel的数据
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Vert_MaxFilter(Dest, T, Height, Width, Height * Channel, Radius);
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Transpose(T, Dest, Height, Width, Height * Channel, Width, Height, Stride);
FreeMemory:
free(T);
return Status;
}
上面代码中还有很多细节,包括分块尾部不完整数据的处理,大家可以自己理解。
有两处删除了部分代码,删除的代码是很容易补上去的,因为我不喜欢我的代码被别人直接复制黏贴。
进一步的分析:
由上面的代码可以看到,要实现整个的过程,我们需要原图3倍大小的额外内存,那么是否有降低这个的可能性呢,是有的,在处理列方向时,我们可以一次性只处理16列或32列,这样g/h数据只各需要Height * 16(32) * sizeof(unsigned char)大小的内存,而且这样做还有一个优势就是在每个分段内部比较时,由于更新的内容较少,可以用一个xmm寄存器保存最值得临时结果,这样就不用去加载上一行的内存数据,少了很多次内存读写的过程,一个简单的示例代码如下所示:
unsigned char *LinePS = Src + StartY * Stride + X;
unsigned char *LinePD = G + StartY * ;
__m128i Max1 = _mm_setzero_si128(), Max2 = _mm_setzero_si128(); // 这样写能减少一次内存加载
for (int Y = StartY; Y < EndY; Y++)
{
Max1 = _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + )), Max1); // 或者使用一条AVX指令
Max2 = _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + )), Max2);
_mm_store_si128((__m128i *)(LinePD + ), Max1);
_mm_store_si128((__m128i *)(LinePD + ), Max2);
LinePS += Stride;
LinePD += ;
}
在我的笔记本中测试,这个的速度要比上面的版本还快一点,并且有占用了更少的内存,一举两得啊。
欢迎大家能提供更快速的算法的实现思路。
本文Demo下载地址: http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,里面的所有算法都是基于SSE实现的。
如果觉得本文对你有帮助,请为本文点个赞。
2019.4.26日更新:
在本文的评论里有网友提出该文的速度还是不够快,比起商业软件还有很大的差距,一直未找到良方,最近有网友说他做的比我这个速度要快,其提到了不需要完全转置。虽未提到代码层面的东西,但是这种事情是一点就通的,要实现起来还是很容易的。
比起全局转置,局部转置可以只需要分配很少的内存,从速度方面考虑,我们在进行了垂直方向的优化后,就要进行水平方向处理,此时,我每次转置32行(或其他16的倍数),然后利用垂直方向的处理技巧处理转置后数据的垂直方向最值,处理完成后在转置到水平方向对应的32行数据中,至于不能被32行整除的那一部分,就用普通的方式处理了。
此种优化后,我们惊喜的发现,速度较之前有2到3倍的提高,如下图所示:
其实,整体来看,修改后的代码计算量上并没有什么减少,那主要耗时降低了,其核心在于减少了内存的Cache Miss,这种技巧在很多算法中也可以加以推广。
最后,我们还注意到一个问题,表面上看垂直方向的代码更为简介,流程也少那些转置的过程,但是最后实测垂直处理的时间和水平处理的时间的占比约为内存·6:4,分析认为这个的主要原因是在垂直方向处理时图比较大,连续访问垂直方向的内存,Cache miss比较多,而水平方向因为是分块处理的,中间用到的临时内存访问时基本无啥Cache miss(宽度为32的临时区都是连续访问的)。
修改后的算法对于评论区里博友提到的4096X8192大小的的灰度图,其耗时大概在70ms,比其说的商业软件的速度还是要慢一倍的。在Opencv中,使用cvErode函数,发现他也非常非常的快,和我这里的速度旗鼓相当,有的时候还快点,查看其代码,发现他核心的地方是调用hal:morph实现的,但是HAL硬件加速层到底是个啥家伙,实在是搞不清楚。
SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法。的更多相关文章
- 基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法。
因未测试其他作者的算法时间和效率,本文不敢自称是最快的,但是速度也可以肯定说是相当快的,在一台I5机器上占用单核的资源处理 3000 * 2000的灰度数据用时约 20ms,并且算法和核心的大小是无关 ...
- SSE图像算法优化系列二十三: 基于value-and-criterion structure 系列滤波器(如Kuwahara,MLV,MCV滤波器)的优化。
基于value-and-criterion structure方式的实现的滤波器在原理上其实比较简单,感觉下面论文中得一段话已经描述的比较清晰了,直接贴英文吧,感觉翻译过来反而失去了原始的韵味了. T ...
- SSE图像算法优化系列十二:多尺度的图像细节提升。
无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...
- SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)
在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...
- SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。
在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是 ...
- SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。
Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...
- SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度
2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...
- SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。
又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...
- SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。
关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...
随机推荐
- 20165235 祁瑛 Java第四周考试总结
20165235 祁瑛 Java第四周考试总结 课后习题p29 p45 代码编写 import java.util.*; class Example2_5{ public static void ma ...
- Selenium3详解(基本操作,定位方法)
如果想使用selenium驱动不同的浏览器,必须单独下载并设置不同的浏览器驱动. 基本操作: 刷新:refresh, 获取浏览器窗口大小:get_window_size 设置浏览器窗口大小:set_w ...
- TF之RNN:实现利用scope.reuse_variables()告诉TF想重复利用RNN的参数的案例—Jason niu
import tensorflow as tf # 22 scope (name_scope/variable_scope) from __future__ import print_function ...
- C++雾中风景13:volatile解惑
笔者入职百度时,二面面试官的让我聊聊C++之中的volatile关键词.volatile在Java和C++之中的差别可谓是天差地别,我只是简单聊了聊Java之中的volatile,面试官对我的回答并不 ...
- 自己总结的C#编码规范--1.命名约定篇
命名约定 我们在命名标识符时(包括参数,常量,变量),应使用单词的首字母大小写来区分一个标识符中的多个单词,如UserName. PascalCasing PascalCasing包含一到多个单词,每 ...
- Adams/Car与Simulink联合仿真方法
必须是Assembly装配体才行,并支持仿真设置.这里使用MDI_Demo_Vehicle模型,输出前缀为test1,输出选择files_only.然后OK输出. 生成的文件如下: 在Plant Ex ...
- 解决SpringBoot的@Autowired无法注入问题
问题:@Autowired无法自动注入 思路:SpringBoot项目的Bean装配默认规则是根据Application类所在的包位置从上往下扫描!"Application类"是指 ...
- SSM整合Activiti
1.pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...
- Shell脚本笔记(三)shell中的数学计算
shell中的数学计算 一.使用方括号 #!/bin/bash a= b= c= res=$[$a * ($c-$b)] echo $res 二.使用(()) +)) ((i=+)) b=$((-*) ...
- 潭州课堂25班:Ph201805201 django 项目 第四十二课 后台 课程相关,用户组管理 (课堂笔记)
在线课程: 当点击进入页面时,显示所有课程 def get(self, request): courses = Course.objects.select_related('category', 't ...