標籤:
轉自 http://blog.sina.com.cn/s/blog_628821950100wh9w.html
C#進行影像處理的幾種方法
本文討論了C#影像處理中Bitmap類、BitmapData類和unsafe代碼的使用以及位元組對齊問題。
Bitmap類
命名空間:System.Drawing
封裝 GDI+ 位元影像,此位元影像由圖形映像及其屬性的像素資料群組成。Bitmap 是用於處理由像素資料定義的映像的對象。
利用C#類進行影像處理,最方便的是使用Bitmap類,使用該類的GetPixel()與SetPixel()來訪問映像的每個像素點。下面是MSDN中的範例程式碼:
public void GetPixel_Example(PaintEventArgs e) { // Create a Bitmap object from an image file. Bitmap myBitmap = new Bitmap("Grapes.jpg"); // Get the color of a pixel within myBitmap. Color pixelColor = myBitmap.GetPixel(50, 50); // Fill a rectangle with pixelColor. SolidBrush pixelBrush = new SolidBrush(pixelColor); e.Graphics.FillRectangle(pixelBrush, 0, 0, 100, 100); }
可見,Bitmap類使用一種優雅的方式來操作映像,但是帶來的效能的降低卻是不可忽略的。比如對一個800*600的彩色映像灰階化,其耗費的時間都要以秒為單位來計算。在實際項目中進行影像處理,這種速度是決對不可忍受的。
BitmapData類
命名空間:System.Drawing.Imaging
指定位元影像映像的屬性。BitmapData 類由 Bitmap 類的 LockBits 和 UnlockBits 方法使用。不可繼承。
好在我們還有BitmapData類,通過BitmapData BitmapData LockBits ( )可將 Bitmap 鎖定到系統記憶體中。該類的公用屬性有:
- Width 擷取或設定 Bitmap 對象的像素寬度。這也可以看作是一個掃描行中的像素數。
- Height 擷取或設定 Bitmap 對象的像素高度。有時也稱作掃描行數。
- PixelFormat
擷取或設定返回此 BitmapData 對象的 Bitmap 對象中像素資訊的格式。
- Scan0 擷取或設定位元影像中第一個像素資料的地址。它也可以看成是位元影像中的第一個掃描行。
- Stride 擷取或設定 Bitmap 對象的跨距寬度(也稱為掃描寬度)。
下面的MSDN中的範例程式碼示範了如何使用 PixelFormat、Height、Width 和 Scan0 屬性;LockBits 和 UnlockBits 方法;以及 ImageLockMode 枚舉。
private void LockUnlockBitsExample(PaintEventArgs e) { // Create a new bitmap. Bitmap bmp = new Bitmap("c:\\fakePhoto.jpg"); // Lock the bitmap‘s bits. Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); System.Drawing.Imaging.BitmapData bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat); // Get the address of the first line. IntPtr ptr = bmpData.Scan0; // Declare an array to hold the bytes of the bitmap. int bytes = bmpData.Stride * bmp.Height; byte[] rgbValues = new byte[bytes]; // Copy the RGB values into the array. System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes); // Set every red value to 255. for (int counter = 0; counter < rgbValues.Length; counter+=3) rgbValues[counter] = 255; // Copy the RGB values back to the bitmap System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes); // Unlock the bits. bmp.UnlockBits(bmpData); // Draw the modified image. e.Graphics.DrawImage(bmp, 0, 150); }
上面的代碼示範了如何用數組的方式來訪問一幅映像,而不在使用低效的GetPixel()和SetPixel()。
unsafe代碼
而在實際中上面的做法仍然不能滿足我們的要求,影像處理是一種運算量比較大的操作,不同於我們寫的一般的應用程式。我們需要的是一種效能可以同C++程式相媲美的影像處理程式。C++是怎麼提高效率的呢,答曰:指標。幸運的是.Net也允許我們使用指標,只能在非安全的程式碼塊中使用指標。何謂非安全的程式碼?
為了保持型別安全,預設情況下,C# 不支援指標運算。不過,通過使用 unsafe 關鍵字,可以定義可使用指標的不安全上下文。在公用語言運行庫 (CLR) 中,不安全的程式碼是指無法驗證的代碼。C# 中的不安全的程式碼不一定是危險的,只是其安全性無法由 CLR 進行驗證的代碼。因此,CLR 只對在完全受信任的程式集中的不安全的程式碼執行操作。如果使用不安全的程式碼,由您負責確保您的代碼不會引起安全風險或指標錯誤。不安全的程式碼具有下列屬性:
- 方法、類型和可被定義為不安全的代碼塊。
- 在某些情況下,通過移除數組界限檢查,不安全的程式碼可提高應用程式的效能。
- 當調用需要指標的本機函數時,需要使用不安全的程式碼。
- 使用不安全的程式碼將引起安全風險和穩定性風險。
- 在 C# 中,為了編譯不安全的程式碼,必須用 /unsafe 編譯應用程式。
正如《C#語言規範》中所說無論從開發人員還是從使用者角度來看,不安全的程式碼事實上都是一種“安全”功能。不安全的程式碼必須用修飾符 unsafe 明確地標記,這樣開發人員就不會誤用不安全功能,而執行引擎將確保不會在不受信任的環境中執行不安全的程式碼。
以下代碼示範如何藉助BitmapData類採用指標的方式來遍曆一幅映像,這裡的unsafe代碼塊中的代碼就是非安全的程式碼。
//建立映像 Bitmap image = new Bitmap( "c:\\images\\image.gif" ); //擷取映像的BitmapData對像 BitmapData data = image.LockBits( new Rectangle( 0 , 0 , image.Width , image.Height ) , ImageLockMode.ReadWrite , PixelFormat.Format24bppRgb ); //迴圈處理 unsafe { byte* ptr = ( byte* )( data.Scan0 ); for( int i = 0 ; i < data.Height ; i ++ ) { for( int j = 0 ; j < data.Width ; j ++ ) { // write the logic implementation here ptr += 3; } ptr += data.Stride - data.Width * 3; } }
毫無疑問,採用這種方式是最快的,所以在實際工程中都是採用指標的方式來訪問映像像素的。
位元組對齊問題
上例中ptr += data.Stride - data.Width * 3,表示跨過無用的地區,其原因是映像資料在記憶體中儲存時是按4位元組對齊的,具體解釋如下:
假設有一張圖片寬度為6,假設是Format24bppRgb格式的(每像素3位元組,在以下的討論中,除非特別說明,否則Bitmap都被認為是24位RGB)。顯然,每一行需要6*3=18個位元組儲存。對於Bitmap就是如此。但對於BitmapData,雖然data.Width還是等於image.Width,但大概是出於顯示效能的考慮,每行的實際的位元組數將變成大於等於它的那個離它最近的4的整倍數,此時的實際位元組數就是Stride。就此例而言,18不是4的整倍數,而比18大的離18最近的4的倍數是20,所以這個data.Stride = 20。顯然,當寬度本身就是4的倍數時,data.Stride = image.Width * 3。
畫個圖可能更好理解。R、G、B 分別代表3個原色分量位元組,BGR就表示一個像素。為了看起來方便我在們每個像素之間插了個空格,實際上是沒有的。X表示補足4的倍數而自動插入的位元組。為了符合人類的閱讀習慣我分行了,其實在電腦記憶體中應該看成連續的一大段。
|-------Stride-----------|
|-------Width---------|
Scan0:
BGR BGR BGR BGR BGR BGR XX
BGR BGR BGR BGR BGR BGR XX
BGR BGR BGR BGR BGR BGR XX
.
.
.
首先用data.Scan0找到第0個像素的第0個分量的地址,這個地址指向的是個byte類型,所以當時定義為byte* ptr。行掃描時,在當前指標位置(不妨看成當前像素的第0個顏色分量)連續取出三個值(3個原色分量。注意,0 1 2代表的次序是B G R。在取指標指向的值時,貌似p[n]和p += n再取p[0]是等價的),然後下移3個位置(ptr += 3,看成指到下一個像素的第0個顏色分量)。做過Bitmap.Width次操作後,就到達了Bitmap.Width * 3的位置,應該要跳過圖中標記為X的位元組了(共有Stride - Width * 3個位元組),代碼中就是 ptr += dataIn.Stride - dataIn.Width * 3。
通過閱讀本文,相信你已經對使用C#進行影像處理可能用到的幾種方法有了一個瞭解。至於採用哪種方式,取決於你的效能要求。其中第一種方式最優雅;第三種方式最快,但不是安全的程式碼;第二種方式取了個折中,保證是安全的程式碼的同時又提高了效率。熟悉C/C++編程的人可能會比較偏向於第三種方式,我個人也比較喜歡第三種方式。
(轉)C#進行影像處理的幾種方法(Bitmap,BitmapData,IntPtr)