想當年在初學C#時,知道C#有unsafe這種東西,可以解開.NET對指標(Pointer)的封印,允許像C語言一樣透過指標直接存取記憶體。對C語言沒有深厚基礎的我,模糊地知道直接存取記憶體效能較佳,卻不很是清楚它的應用時機。最近胡亂玩了一些視覺元素識別的題目,參考一些圖形運算前輩的範例程式,才訝異地發現,原來密集大量運算的場合,就是unsafe橫掃千軍的絕佳舞台。

舉個簡單的例子,假設我們在某本書的綠色書背擷取到一塊包含ISBN條碼的影像,打算透過演算法找出條碼區所在位置。由於綠色書皮與白色條碼區在顏色上有明顯區別,因此可使用比對顏色RGB值的方式,將"不夠白"的地方一律塗黑,以二分方將有色區跟白色區明顯區隔開來。(請不要問我如果書皮是白色的怎麼辦? 這裡只是舉一個方便展示程式的範例咩! 只是範例只是範例只是範例...)

這程式一點也不難寫, 利用Bitmap載入圖檔,然後借重GetPixel讀取圖片中每一點的顏色,判斷後決定改為純白或全黑,再以SetPixel寫回去,大功告成!

    private void ImgProcTest1()
    {
        Bitmap bmp =  Bitmap.FromFile(
            "c:\\temp\\100617\\isbn.bmp") as Bitmap;
        int limit = 0x40;
 
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int x = 0; x < bmp.Width; x++)
            for (int y = 0; y < bmp.Height; y++)
            {
                Color c = bmp.GetPixel(x, y);
                if (c.R < limit ||
                    c.B < limit ||
                    c.G < limit)
                    bmp.SetPixel(x, y, Color.Black);
                else
                    bmp.SetPixel(x, y, Color.White);
            }
        sw.Stop();
        MessageBox.Show(sw.ElapsedMilliseconds.ToString());
        pictureBox1.Image = bmp;
    }

用上面這段程式解析480*320的圖檔,在我的E6400 Dual Core機器上耗時約550ms,約0.5秒搞定,速度還可接受。

不過,直覺上存取每一像素都要呼叫函數的寫法效率不佳,而Bitmap有個LockBits,支援以byte[]方式處理像素,速度應該會提升一些。參考MSDN範例,我們改用LockBits鎖定記憶體,接著用Marshal.Copy將資料複製到byte[],判斷修改完再以Marshal.Copy將資料存回Bitmap,就可得到與上述程式相同的結果。

    private void ImgProcTest2()
    {
        Bitmap bmp = Bitmap.FromFile(
            "c:\\temp\\100617\\isbn.bmp") as Bitmap;
        int limit = 0x40;
 
        Stopwatch sw = new Stopwatch();
        sw.Start();
        BitmapData data =
            bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
            ImageLockMode.ReadWrite,
            PixelFormat.Format24bppRgb);
        IntPtr ptr = data.Scan0;
        int len = bmp.Width * bmp.Height * 3;
        byte[] buff = new byte[len];
        Marshal.Copy(ptr, buff, 0, len);
        int idx = 0;
        for (int x = 0; x < bmp.Width; x++)
            for (int y = 0; y < bmp.Height; y++)
            {
                byte c = (buff[idx] < limit ||
                            buff[idx+1] < limit ||
                            buff[idx+2] < limit) ? (byte)0 : (byte)0xff;
                buff[idx] = buff[idx + 1] = buff[idx + 2] = c;
                idx += 3;
            }
        Marshal.Copy(buff, 0, ptr, len);
        bmp.UnlockBits(data);
        sw.Stop();
        MessageBox.Show(sw.ElapsedMilliseconds.ToString());
        pictureBox1.Image = bmp;
    }

Wow, 測試結果,改用byte[]後只需約40ms,速度增快了14倍!!! 夠快了嗎? 如果今天我們處理的影像來自攝影裝置,那麼40ms代表一秒鐘最多可運算25次,若演算法再複雜一點,這種速度仍不夠理想,只是,還能再快嗎??

嘿... 該unsafe上場了! LockBits後,我們改透過byte*指標直接操作Bitmap記憶體,冒著讀寫不慎程式就會當掉的危險,我們能把速度提升到何等境界呢?

    private void ImgProcTest3()
    {
        Bitmap bmp = Bitmap.FromFile(
            "c:\\temp\\100617\\isbn.bmp") as Bitmap;
        int limit = 0x40;
 
        Stopwatch sw = new Stopwatch();
        sw.Start();
        BitmapData data =
            bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
            ImageLockMode.ReadWrite,
            PixelFormat.Format24bppRgb);
        unsafe
        {
            byte* ptr = (byte*)(data.Scan0);
            int padding = data.Stride - (bmp.Width * 3);
 
            for (int i = 0; i < data.Height; i++)
            {
                for (int j = 0; j < data.Width; j++)
                {
                    //ptr[0] -> B, ptr[1] -> G, ptr[2] -> R
                    byte c = (ptr[0] < limit ||
                                ptr[1] < limit ||
                                ptr[2] < limit) ? (byte)0 : (byte)0xff;
                    ptr[0] = ptr[1] = ptr[2] = c;
                    ptr += 3;
                }
                ptr += padding;
            }
        }
        bmp.UnlockBits(data);
 
        sw.Stop();
        MessageBox.Show(sw.ElapsedMilliseconds.ToString());
        pictureBox1.Image = bmp;
    }

2ms!! 好說好說,比用.NET byte[]快了20倍而已。這就是為什麼儘管unsafe很不安全,卻仍有其不可取代性的原因吧~~~ 有了unsafe,.NET也有機會寫出效能逼近C的程式碼囉! 但一定要當心,unsafe顧名思義本是一種不安全的寫法,少了.NET的保護,讀寫錯位置很有可能造成嚴重當機,算是簽了生死狀去飆速度,在撰寫程式計算記憶體位址時要格外小心。


Comments

# by TeYoU

「筆記」LockBits 可以搭配這文章一起看 http://www.bobpowell.net/lockingbits.htm

# by Jeffrey

to TeYoU, 謝謝補充,好一篇鞭辟入裡的佳文!

# by 山姆先生

黑大, 看到這一篇我突然想到一個問題, 我一直想要寫一隻在Excel環境下, 能夠將插入(或複製貼上)的圖片反黑的Scripts, 不知道利用VBS有沒有可能達成呢? 還是有其它的方法? 至於為什麼要反黑, 其實是因為我們經常會複製一些類似Dos的黑底白字畫面, 相當浪費印表機的炭粉, 很不環保~ 所以現在都是利用Copy去小畫家自己反黑後再重新貼回Excel, 但還是覺得太浪費時間了... 以上 謝謝

# by cc

可惜。。。Silverlight中不能用unsafe

# by Felix

請問範例中這個 method 的參數有寫錯嗎? Marshal.Copy(buff, 0, ptr, len); prototype 如下: public static void Copy(IntPtr source, byte[] destination, int startIndex, int length)

# by Jeffrey

to Felix, Copy 有多個 Overloading,範例是用這個 public static void Copy (byte[] source, int startIndex, IntPtr destination, int length); https://learn.microsoft.com/zh-tw/dotnet/api/system.runtime.interopservices.marshal.copy?view=net-7.0#system-runtime-interopservices-marshal-copy(system-byte()-system-int32-system-intptr-system-int32)

Post a comment