【短道速滑一】OpenCV中cvResize函數使用雙線性插值縮小圖像到長寬大小一半時速度飛快(比最近鄰還快)之異象解析和自我實現。

  今天,一個朋友想使用個人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實現的。

 

相關文章
相關標籤/搜索