今天,一个朋友想使用个人SSE优化Demo里的双线性插值算法,他已经在项目里使用了OpenCV,所以,我就建议他直接使用OpenCV,朋友的程序很是注意效率和实时性(由于是处理视频),所以但愿我能测试下个人速度和OpenCV相比到底那一个更有速度优点,刚好前一段时间也有朋友有这方面的需求,所以我就随意编写了一个测试程序,以下所示:算法
IplImage *T = cvLoadImage("F:\\1.JPG"); IplImage *SrcImg = cvCreateImage(cvSize(T->width, T->height), IPL_DEPTH_8U, 1); cvCvtColor(T, SrcImg, CV_BGR2GRAY); //IplImage *SrcImg = cvLoadImage("F:\\3.jpg"); cvNamedWindow("处理前", CV_WINDOW_AUTOSIZE); cvShowImage("处理前", SrcImg); IplImage *DestImg = cvCreateImage(cvSize(SrcImg->width / 2, SrcImg->height / 2), SrcImg->depth, SrcImg->nChannels); LARGE_INTEGER t1, t2, tc; QueryPerformanceFrequency(&tc); QueryPerformanceCounter(&t1); for(int i=0; i<100; i++) cvResize(SrcImg, DestImg, CV_INTER_CUBIC); QueryPerformanceCounter(&t2); printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart) * 1000.0f / tc.QuadPart); cvNamedWindow("处理后", CV_WINDOW_AUTOSIZE); cvShowImage("处理后", DestImg); cvReleaseImage(&SrcImg); cvReleaseImage(&DestImg); cvReleaseImage(&T);
我使用了一张3000*2000的大图进行测试,令我很是诧异的是,执行100次这个函数耗时竟然只有 Use Time:82.414300 ms,每一帧都不到1ms,目标图像的大小但是1500*1000的呢,立马打开我本身的Demo,一样的环境下测试,100次耗时达到了450ms,相差太多了,要知道,我那个但是SSE优化后的啊。有点不敢相信这个事实。ide
接着,我把CV_INTER_LINEAR(双线性)改成CV_INTER_NN(最近临),出来的结果是Use Time:78.921600 ms,注意到没有,时间比双线性的还要多,感受这彻底不合乎逻辑啊。函数
稍微冷静下来,我认为这绝对不符合真理,可是我心中已经隐隐约约知道大概为何会出现这个状况,因而,我又作了下面几个测试。性能
第一、换一副图像看看,我把源图像的大小改成3001*2000,测试结果为:Use Time:543.837400 ms。测试
把源图像的大小改成3000*2001,测试结果为:Use Time:541.567800 ms。优化
把源图像的大小改成3001*2001,测试结果为:Use Time:547.325600 ms。spa
第二:源图像仍是使用3000*2000大小,把DestImg的大小修改成1501*1000,测试结果为:Use Time:552.432800 ms。code
把DestImg的大小修改成1500*1001,测试结果为:Use Time:549.956400 ms。orm
把DestImg的大小修改成1501*1001,测试结果为:Use Time:551.371200 ms。视频
这两个测试代表,这种状况只在:
1、源图像的宽度和高度均为2的倍数时;
2、目标图像的宽度和高度都必须为源图像的一半时;
时方有可能出现,那么他们是充分条件了吗?接着作试验。
第三:把插值方法改成其余的方式,好比CV_INTER_CUBIC(三次立方),若其余参数都不变,测试结果为:Use Time:921.885900 ms。
一样适使用三次立方,源图大小修改成3000*2001,测试结果为:Use Time:953.748100 ms。
适用三次立方,源图大小不变,目标图修改1501*1000,测试结果为:Use Time:913.735600 ms。
可见此时不管怎么调整输入输出,基本的耗时都差很少,换成CV_INTER_AREA或CV_INTER_NN也能获得一样的结果。
这第三个测试代表,此异常现象还只有在:
三:使用了双线性插值算法;
时才可能出现。这些条件就足够了吗?接着看。
第四:其余条件暂时不动,把测试代码修改以下:
IplImage *SrcImg = cvLoadImage("F:\\1.jpg"); cvNamedWindow("处理前", CV_WINDOW_AUTOSIZE); cvShowImage("处理前", SrcImg); IplImage *DestImg = cvCreateImage(cvSize(SrcImg->width / 2, SrcImg->height / 2), SrcImg->depth, SrcImg->nChannels); LARGE_INTEGER t1, t2, tc; QueryPerformanceFrequency(&tc); QueryPerformanceCounter(&t1); for(int i=0; i<100; i++) cvResize(SrcImg, DestImg, CV_INTER_CUBIC); QueryPerformanceCounter(&t2); printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart) * 1000.0f / tc.QuadPart); cvNamedWindow("处理后", CV_WINDOW_AUTOSIZE); cvShowImage("处理后", DestImg); cvReleaseImage(&SrcImg); cvReleaseImage(&DestImg);
即便用彩色图像进行测试,运行的结果为:Use Time:271.705700 ms。看这个的时间和灰度的82ms相比,一猜就知道仍是作了特别的处理。
可是咱们仍是多作几个测试,咱们将输出图像的大小修改成1501*1000、1500*100一、1501*1001时,100次的耗时在1367ms,若是输入图像修改成长或宽为非偶数时,耗时也差很少要1300多ms,说明OpenCV对彩色图像的这种状况也有作优化处理。
所以,这个算法对彩色也是有效的。
以上三个条件在一块儿构成了出现上述异常现象的充分必要条件。下面根据我我的的想法来谈谈OpenCV为何会出现这个现象(我没有去翻OpenCV的代码)。
我的认为,出现该现象核心仍是由双线性插值算法的本质引发的。双线性插值算法在插值时涉及到周边四个像素,当源图像宽度和高度都为2的倍数,若是此时的目标图像的长度和高度又刚好是源图像宽度和高度的一半,这个时候的双线性插值就退化为对原图像行列方向每隔一个像素求平均值(四个像素)的过程。若是不是双线性插值,他涉及到领域范围就不是4个,好比三次立方就涉及到16个领域,而非2的倍数或非一半的大小则没法规整到0.25的权重(4个像素的平均值)。
对于这个特例,咱们用C语言能够简单的写出其计算过程:
int IM_ZoomIn_Half_Bilinear(unsigned char *Src, unsigned char *Dest, int SrcW, int SrcH, int StrideS, int DstW, int DstH, int StrideD) { int Channel = StrideS / SrcW; if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE; if ((SrcW <= 0) || (SrcH <= 0) || (DstW <= 0) || (DstH <= 0)) return IM_STATUS_INVALIDPARAMETER; if ((Channel != 1) && (Channel != 3) && (Channel != 4)) return IM_STATUS_INVALIDPARAMETER; if ((SrcW % 2 != 0) || (SrcH % 2 != 0)) return IM_STATUS_INVALIDPARAMETER; if ((DstW != SrcW / 2) || (DstH != SrcH / 2)) return IM_STATUS_INVALIDPARAMETER; if (Channel == 1) { for (int Y = 0; Y < DstH; Y++) { unsigned char *LinePD = Dest + Y * StrideD; unsigned char *LineP1 = Src + Y * 2 * StrideS; unsigned char *LineP2 = LineP1 + StrideS; for (int X = 0; X < DstW; X++, LineP1 += 2, LineP2 += 2) { LinePD[X] = (LineP1[0] + LineP1[1] + LineP2[0] + LineP2[1] + 2) >> 2; } } } else if (Channel == 3) { for (int Y = 0; Y < DstH; Y++) { unsigned char *LinePD = Dest + Y * StrideD; unsigned char *LineP1 = Src + Y * 2 * StrideS; unsigned char *LineP2 = LineP1 + StrideS; for (int X = 0; X < DstW; X++) { LinePD[0] = (LineP1[0] + LineP1[3] + LineP2[0] + LineP2[3] + 2) >> 2; LinePD[1] = (LineP1[1] + LineP1[4] + LineP2[1] + LineP2[4] + 2) >> 2; LinePD[2] = (LineP1[2] + LineP1[5] + LineP2[2] + LineP2[5] + 2) >> 2; LineP1 += 6; LineP2 += 6; LinePD += 3; } } } }
代码很是简单,注意到计算式里最后的+2是为了进行四舍五入。
咱们先测试下灰度图,使用上述代码在一样的环境下能够得到: Use Time:225.456300 ms 的成绩,使用循环内2路或4路并行的方式大约能将成绩提升到190ms左右,可是和OpenCV的速度相比仍是有蛮大的差距。这么简答的代码,咱们能够直接用SIMD指令进行优化:
咱们先使用SSE进行尝试:
__m128i Zero = _mm_setzero_si128(); for (int Y = 0; Y < DstH; Y++) { unsigned char *LinePD = Dest + Y * StrideD; unsigned char *LineP1 = Src + Y * 2 * StrideS; unsigned char *LineP2 = LineP1 + StrideS; for (int X = 0; X < Block * BlockSize; X += BlockSize, LineP1 += BlockSize * 2, LineP2 += BlockSize * 2) { __m128i Src1 = _mm_loadu_si128((__m128i *)LineP1); __m128i Src2 = _mm_loadu_si128((__m128i *)LineP2); // A0+B0 A1+B1 A2+B2 A3+B3 A4+B4 A5+B5 A6+B6 A7+B7 __m128i Sum_L = _mm_add_epi16(_mm_cvtepu8_epi16(Src1), _mm_cvtepu8_epi16(Src2)); // A8+B8 A9+B9 A10+B10 A11+B11 A12+B12 A13+B13 A14+B14 A15+1B15 __m128i Sum_H = _mm_add_epi16(_mm_unpackhi_epi8(Src1, Zero), _mm_unpackhi_epi8(Src2, Zero)); // A0+A1+B0+B1 A2+A3+B2+B3 A4+A5+B4+B5 A6+A7+B6+B7 A8+A9+B8+B9 A10+A11+B10+B11 A12+A13+B12+B13 A14+A15+B14+1B15 __m128i Sum = _mm_hadd_epi16(Sum_L, Sum_H); // (A0+A1+B0+B1+2)/4 (A2+A3+B2+B3)/4 (A4+A5+B4+B5)/4 (A6+A7+B6+B7)/4 (A8+A9+B8+B9)/4 (A10+A11+B10+B11)/4 (A12+A13+B12+B13)/4 (A14+A15+B14+1B15)/4 __m128i Result = _mm_srli_epi16(_mm_add_epi16(Sum, _mm_set1_epi16(2)), 2); _mm_storel_epi64((__m128i *)(LinePD + X), _mm_packus_epi16(Result, Zero)); } for (int X = Block * BlockSize; X < DstW; X++, LineP1 += 2, LineP2 += 2) { LinePD[X] = (LineP1[0] + LineP1[1] + LineP2[0] + LineP2[1] + 2) >> 2; } }
对SSE优化来讲,也没啥,加载数据,将其转换成16位(字节相加确定会溢出,到16位后4个数相加确定会在16位的范围内),注意上面的最为精华的部分为_mm_hadd_epi16的使用,他的水平累加过程刚好能够完成最后的列方向的处理,若是咱们先用这个函数完成A0+A1这样的工做,那若是要完成一样的工做,后续就要多了一些shuffle过程了,这样就下降了速度。
这段SIMD指令通过测试,100次循环耗时在90-100ms之间徘徊,和OpenCV的结果有点差很少了。
若是咱们使用AVX指令进行优化,总体基本和SSE差很少,可是局部细节上仍是有所差别的,以下所示:
for (int Y = 0; Y < DstH; Y++) { unsigned char *LinePD = Dest + Y * StrideD; unsigned char *LineP1 = Src + Y * 2 * StrideS; unsigned char *LineP2 = LineP1 + StrideS; __m256i Zero = _mm256_setzero_si256(); for (int X = 0; X < Block * BlockSize; X += BlockSize, LineP1 += BlockSize * 2, LineP2 += BlockSize * 2) { __m256i Src1 = _mm256_loadu_si256((__m256i *)LineP1); __m256i Src2 = _mm256_loadu_si256((__m256i *)LineP2); // 注意这里使用unpack的方式来实现8位和16位的转换,若是使用_mm256_cvtepu8_epi16则低位部分须要一个__m128i变量,而 // 高位使用_mm256_unpackhi_epi8则须要一个__m256i变量,这样会存在重复加载现象的。 __m256i Sum_L = _mm256_add_epi16(_mm256_unpacklo_epi8(Src1, Zero), _mm256_unpacklo_epi8(Src2, Zero)); __m256i Sum_H = _mm256_add_epi16(_mm256_unpackhi_epi8(Src1, Zero), _mm256_unpackhi_epi8(Src2, Zero)); __m256i Sum = _mm256_hadd_epi16(Sum_L, Sum_H); __m256i Result = _mm256_srli_epi16(_mm256_add_epi16(Sum, _mm256_set1_epi16(2)), 2); // 注意_mm256_packus_epi16 并非_mm_packus_epi16的线性扩展,很恶心的作法 _mm_storeu_si128((__m128i *)(LinePD + X), _mm256_castsi256_si128(_mm256_permute4x64_epi64(_mm256_packus_epi16(Result, Zero), _MM_SHUFFLE(3, 1, 2, 0)))); } for (int X = Block * BlockSize; X < DstW; X++,LineP1 += 2, LineP2 += 2) { LinePD[X] = (LineP1[0] + LineP1[1] + LineP2[0] + LineP2[1] + 2) >> 2; } }
特别注意到的是最后_mm256_packus_epi16指令的使用,他和_mm256_add_epi16或者 _mm256_srli_epi16不同,并非对SSE指令简单的从128位扩展到256位,咱们从其简单的数学解释就能够看到:
_mm_add_epi16 _mm256_add_epi16
add指令就是直接从8次一次性计算简单的扩展到16次一次性计算,在来看packus指令:
_mm_packus_epi16 _mm256_packus_epi16
_mm256_packus_epi16 实际上能够当作是对两个__m128i变量单独进行处理,而不是把他们当作一个总体,这样一样的算法,咱们就在AVX中就不能使用一样SSE指令了,好比最后的保存的语句,咱们必须使用一个_mm256_permute4x64_epi64指令来进行一下shuffle调序操做。
这种不便利性也是我不肯意将大部分SSE指令扩展到AVX的一个重要障碍之一。
使用AVX编写的程序优化后的耗时大约在80ms左右波动,这个已经很是接近OpenCV的速度了,至此,咱们有理由相信OpenCV在实现这个的过程当中应该也采起了相似我上述的优化方式进行处理(没有仔细的翻OpenCV的代码,请有看过的朋友指导下)。
那么咱们再谈谈为何这个速度比最近邻插值还要快吧,最近邻算法中,不存在插值,直接在源图像中选择一个坐标位置的点做为新的像素值,在放大时其会出现多行像素相同的特性,这个特性能够用来加快算法执行速度,可是对于缩小,只有一个点一个点的计算,至多能够用查找表提早计算好坐标,通过尝试,这算法是不易用多媒体指令进行优化的,并且即便用,也无明显的速度提高。而对于本文的双线性的特例,其并行的特性很是好,并且自己的计算量也不是很大,所以,就出现用SIMD优化后速度还比最近邻还快的结果。
对于彩色图像,普通的C语言代码也很简单,上面也已经贴出代码,这段代码执行100次大概耗时在500ms左右,注意这个时候对他进行SIMD指令优化就不是一件很直接和很简单的事情了,由于BGRBGR这样的排列顺序到底没法直接使用灰度模式的指令扩展,必需要将BGR从新排序,变为BBB GGG RRR这样的模式,而后单独对份量进行处理,处理完成后再合成为BGR排列,所以,这样排列须要一次性加载48个字节(SSE),用3个SSE寄存器保存数据,这个时候若是使用AVX指令就显得有点繁琐了,并且就是用AVX带来的性能收益也微乎其微。 一样的,这种计算量不大的算法,用SIMD指令优化后的收益并非特别明显,对于彩色图像,SSE优化后其时间大概能缩短到300ms,这个速度要比OpenCV的稍微慢一点。
随着如今的视频显示设备愈来愈先进,采集的图像也愈来愈大,好比如今4K的高清摄像头也不在少数,在有些实时要求性很好的场合,咱们必须考虑处理能力,将图像缩小在处理是经常使用的手段,并且,我想长宽各一半的这种缩小场合在此状况下也应该是很常见的,所以,特列的特别优化就显得很是有意义。
还有,通常状况下图像屡次缩小2倍要比直接缩小大于2倍的效果更好,或者说经过屡次缩放获得的结果通常要比直接一次性缩放获得的结果要更好,好比,下面左图是直接缩放到原图1/4长宽的结果,右图是先缩小一半,在缩小一半的结果,在风车的边缘能够看到后者更为平滑。
在耗时上,好比上面这个操做,直接缩小到1/4因不是特殊处理,而经过2次一半的处理每次都是特殊算法,虽然次数多了,可是总耗时也就比直接缩小1/4多了0.5倍,效果却要好一点,对于那些重效果的地方,仍是很是有意义的,特别是若是是处理4K的图,这种处理也有很好的借鉴意义。
最后说一下,进一步测试表面我自行优化的缩放算法和OpenCV的相比灰度图上基本差很少,彩色图像大概要快20%左右。
本文Demo下载地址: http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,位于Edit-Resample菜单下,里面的全部算法都是基于SSE实现的。