【查蟲日誌】快速判斷一副灰度影象中是否只有黑色和白色值(即是否為二值影象)過程中bool變數的是是非非。

Imageshop發表於2019-06-30

  二值影象我們在影象處理過程中是經常遇到的,有的時候我們在進行一個演算法處理前,需要判斷下一副影象的資料是否符合二值圖的需求,這個時候我們可以寫個簡單的函式來做個判斷,比如我寫了一個很簡單的的程式碼如下:

bool IM_IsBinaryImage_C(unsigned char *Src, int Width, int Height, int Stride)
{
    int Channel = Stride / Width;
    if (Src == NULL)                            return false;
    if ((Width <= 0) || (Height <= 0))            return false;
    if (Channel != 1)                            return false;
    for (int Y = 0; Y < Height; Y++)                                        
    {
        unsigned char *LinePS = Src + Y * Stride;
        for (int X = 0; X < Width * Channel; X++)
        {
            if ((LinePS[X] != 255) && (LinePS[X] != 0))    return false;
            //if (((LinePS[X] == 255) || (LinePS[X] == 0)) == false) return false;
        }
    }
    return true;
}

  即如果存在一個畫素如果不為255,也不為0,則這副圖就不是二值圖,立即可以返回了,而無需進行後續的判斷了。  

  當一副圖不是二值圖時,通常,我們很快就能返回結果了,那麼最壞的情況就是他恰好是二值圖,這樣,我們就要遍歷完所有的畫素。我們測試過對於16MB的二值圖(4000*4000),測試需要15ms的時間,為了能儘量減少耗時,可以使用如下的SIMD指令來優化這個判斷:

bool IM_IsBinaryImage_SSE_Bug(unsigned char *Src, int Width, int Height, int Stride)
{
    int Channel = Stride / Width;
    if (Src == NULL)                            return false;
    if ((Width <= 0) || (Height <= 0))            return false;
    if (Channel != 1)                            return false;
    int BlockSize = 16, Block = (Width * Channel)/ BlockSize;

    for (int Y = 0; Y < Height; Y++)                                        //    速度提升約16倍
    {
        unsigned char *LinePS = Src + Y * Stride;
        for (int X = 0; X < Block * BlockSize; X += BlockSize)
        {
            __m128i SrcV = _mm_loadu_si128((__m128i *)(LinePS + X));
            __m128i MaskW = _mm_cmpeq_epi8(SrcV, _mm_set1_epi8(255));        
            __m128i MaskB = _mm_cmpeq_epi8(SrcV, _mm_setzero_si128());
            __m128i Mask = _mm_or_si128(MaskW, MaskB);
            if (_mm_movemask_epi8(Mask) != 65535)    return false;            //    if (((LinePS[X] == 255) || (LinePS[X] == 0)) == false) return false;
        }
        for (int X = Block * BlockSize; X < Width * Channel; X++)
        {
            if ((LinePS[X] != 255) && (LinePS[X] != 0))    return false;
        }
    }
    return true;
}

  由於SIMD指令裡沒有_mm_cmpneq_epi8函式,我們該用程式碼1片段裡被註釋掉的那種邏輯來判斷一個畫素是否是黑色和白色,這裡當然也有一些技巧,比如_mm_movemask_epi8指令的運用。我們判斷這個畫素是否等於255和0,當然,一個畫素不可能同時滿足這兩個條件,不滿足的Mask返回0,滿足則Mask返回255,所以如果他是黑色和白色,你們這兩個Mask進行或操作肯定就為255,否則或操作後就為0,SIMD中這樣的比較可以一次性進行16個畫素,如果這16個畫素都符合條件,那麼或操作後的mask都為255,這樣通過使用_mm_movemask_epi8來判斷這個mask就完成了16個畫素的判斷。

  很顯然,這個過程的效率要高很多,測試16MB的真二值圖,也就1ms就完成了判斷。

  好,我用上面的那個程式碼寫成DLL,供C#呼叫,相關的函式宣告如下:

[DllImport("IsBinaryImage.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true)]
 private static extern bool IM_IsBinaryImage_C(byte* Src, int Width, int Height, int Stride);
[DllImport("IsBinaryImage.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern bool IM_IsBinaryImage_SSE_Bug(byte* Src, int Width, int Height, int Stride);

  可出來的結果令我非常詫異,我測試了下面這2幅圖:

           

            測試圖1                               測檢視2 (頁面壓縮了)

  這兩幅圖都不是二值圖,他們在某些邊緣位置都有抗鋸齒操作。但是那個IM_IsBinaryImage_C檢測圖1不是二值影象,檢測圖2 是二值影象,而IM_IsBinaryImage_SSE_Bug則檢測圖1是二值影象,圖2不是二值影象。開始我以為是我的SSE程式碼寫錯了,我就又換了一種寫法,如下所示:

bool IM_IsBinaryImage_SSE(unsigned char *Src, int Width, int Height, int Stride)
{
    int Channel = Stride / Width;
    if (Src == NULL)                            return false;
    if ((Width <= 0) || (Height <= 0))            return false;
    if (Channel != 1)                            return false;
    int BlockSize = 16, Block = (Width * Channel) / BlockSize;
    bool Flag = true;
    for (int Y = 0; Y < Height; Y++)                                        //    速度提升約16倍
    {
        unsigned char *LinePS = Src + Y * Stride;
        if (Flag == false)    break;
        for (int X = 0; X < Block * BlockSize; X += BlockSize)
        {
            __m128i SrcV = _mm_loadu_si128((__m128i *)(LinePS + X));
            __m128i MaskW = _mm_cmpeq_epi8(SrcV, _mm_set1_epi8(255));        //    _mm_cmpeq_epi8是自帶的,如果使用_mm_cmpneq_epu8則慢了一些。
            __m128i MaskB = _mm_cmpeq_epi8(SrcV, _mm_setzero_si128());
            __m128i Mask = _mm_or_si128(MaskW, MaskB);
            if (_mm_movemask_epi8(Mask) != 65535)
            {
                Flag = false;                            //    if ((LinePS[X] == 255) || (LinePS[X] == 0)) = false, return  false
                break;
            }
        }
        for (int X = Block * BlockSize; X < Width * Channel; X++)
        {
            if ((LinePS[X] != 255) && (LinePS[X] != 0))
            {
                Flag = false;
                break;
            }
        }
    }
    return Flag;
}

  這個時候測繪對所有的影象結果都正確了。

  但是,我覺得程式碼片段2應該是不會有任何錯誤的啊。為什麼會出現這種現象呢。

  後面從網上查了下,C++的bool變數就只有true和false, 是位元組變數,這個可以用printf("%d", sizeof(false));來驗證,會列印1。而在其他語言中,似乎是int型別。但是我在C#中用 MessageBox.Show(sizeof(bool).ToString());  似乎也是彈出1。

  但是,當我們把這些函式的返回值都改為int後,在C#中呼叫就正常了,比如:

int IM_IsBinaryImage_C(unsigned char *Src, int Width, int Height, int Stride)

  也就是說上述的IM_IsBinaryImage_SSE_Bug函式體並無Bug。這到底是怎麼回事,還請萬能的網路高手有空予以解疑。

  附上測試工程和程式碼:https://files.cnblogs.com/files/Imageshop/ISBinaryImage.rar

相關文章