C#進(jìn)行圖像處理的常見(jiàn)方法(Bitmap,BitmapData,IntPtr)使用詳解
介紹
在C#中,進(jìn)行圖像處理時(shí)主要會(huì)使用到 System.Drawing 命名空間中的幾個(gè)關(guān)鍵類(lèi),其中Bitmap、BitmapData和IntPtr是進(jìn)行高效像素操作的重要工具。以下是如何利用這些類(lèi)進(jìn)行圖像處理的方法概述:
1.Bitmap 類(lèi):
System.Drawing.Bitmap 是一個(gè)封裝了位圖數(shù)據(jù)的類(lèi),它允許你加載、保存、顯示和操作圖像文件或內(nèi)存中的位圖資源。
使用 new Bitmap(width, height, pixelFormat) 創(chuàng)建一個(gè)新的空白位圖。
通過(guò) Bitmap.FromFile(path) 或 Bitmap.FromStream(stream) 加載圖片文件。
提供 GetPixel(x, y) 和 SetPixel(x, y, color) 方法用于獲取和設(shè)置單個(gè)像素的顏色,但請(qǐng)注意,這種方法對(duì)于大規(guī)模圖像處理效率較低。
2.BitmapData 類(lèi):
BitmapData 代表了一個(gè)鎖定的Bitmap對(duì)象的像素?cái)?shù)據(jù)緩沖區(qū),可以直接對(duì)像素進(jìn)行讀寫(xiě)操作以提高性能。
使用 Bitmap.LockBits(Rectangle area, ImageLockMode mode, PixelFormat format, out BitmapData data) 方法可以鎖定Bitmap的部分或全部區(qū)域,從而獲得指向該區(qū)域像素?cái)?shù)據(jù)的指針。
data.Scan0 屬性是一個(gè)IntPtr類(lèi)型,指向第一個(gè)像素的數(shù)據(jù)地址。
解鎖位圖數(shù)據(jù)需要調(diào)用 Bitmap.UnlockBits(BitmapData) 來(lái)釋放資源并確保圖形設(shè)備正確更新。
3.IntPtr 類(lèi)型與 Marshal 類(lèi):
IntPtr 是一個(gè)表示非托管指針(即內(nèi)存地址)的數(shù)據(jù)類(lèi)型。
在處理BitmapData時(shí),通常會(huì)將Scan0屬性所指向的內(nèi)存塊直接映射到托管代碼中,以便于進(jìn)行快速的像素操作。
使用 System.Runtime.InteropServices.Marshal.Copy(IntPtr source, byte[] destination, int startIndex, int length) 將未托管的內(nèi)存塊內(nèi)容復(fù)制到托管數(shù)組中,這樣可以在C#中更方便地進(jìn)行像素遍歷和修改。
修改完成后,可以通過(guò)類(lèi)似方法將修改后的數(shù)組內(nèi)容復(fù)制回BitmapData所指向的內(nèi)存區(qū)域。
綜合以上,一個(gè)高效的圖像處理流程可能是這樣的:
- 創(chuàng)建或加載Bitmap對(duì)象。
- 鎖定Bitmap的像素?cái)?shù)據(jù),得到BitmapData。
- 使用Marshal.Copy將BitmapData的像素?cái)?shù)據(jù)復(fù)制到本地?cái)?shù)組中。
- 對(duì)數(shù)組進(jìn)行所需的圖像處理操作(如:調(diào)整亮度、對(duì)比度、濾鏡等)。
- 再次使用Marshal.Copy將處理過(guò)的數(shù)組數(shù)據(jù)復(fù)制回BitmapData的內(nèi)存中。
- 解鎖BitmapData,使得GDI+能夠自動(dòng)更新對(duì)應(yīng)的Bitmap圖像。
這種方式避免了頻繁調(diào)用GetPixel和SetPixel函數(shù)帶來(lái)的性能瓶頸,實(shí)現(xiàn)了更快的圖像處理速度。
Bitmap類(lèi)
命名空間:System.Drawing
封裝 GDI+ 位圖,此位圖由圖形圖像及其屬性的像素?cái)?shù)據(jù)組成。
Bitmap 是用于處理由像素?cái)?shù)據(jù)定義的圖像的對(duì)象。
利用C#類(lèi)進(jìn)行圖像處理,最方便的是使用Bitmap類(lèi),使用該類(lèi)的GetPixel()與SetPixel()來(lái)訪(fǎng)問(wèn)圖像的每個(gè)像素點(diǎn)。下面是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); }
可見(jiàn),Bitmap類(lèi)使用一種優(yōu)雅的方式來(lái)操作圖像,但是帶來(lái)的性能的降低卻是不可忽略的。比如對(duì)一個(gè)800*600的彩色圖像灰度化,其耗費(fèi)的時(shí)間都要以秒為單位來(lái)計(jì)算。在實(shí)際項(xiàng)目中進(jìn)行圖像處理,這種速度是決對(duì)不可忍受的。
BitmapData類(lèi)
命名空間:System.Drawing.Imaging
指定位圖圖像的屬性。BitmapData 類(lèi)由 Bitmap 類(lèi)的 LockBits 和 UnlockBits 方法使用。不可繼承。
好在我們還有BitmapData類(lèi),通過(guò)BitmapData BitmapData LockBits ( )可將 Bitmap 鎖定到系統(tǒng)內(nèi)存中。該類(lèi)的公共屬性有:
Width 獲取或設(shè)置 Bitmap 對(duì)象的像素寬度。這也可以看作是一個(gè)掃描行中的像素?cái)?shù)。
Height 獲取或設(shè)置 Bitmap 對(duì)象的像素高度。有時(shí)也稱(chēng)作掃描行數(shù)。
PixelFormat 獲取或設(shè)置返回此 BitmapData 對(duì)象的 Bitmap 對(duì)象中像素信息的格式。
Scan0 獲取或設(shè)置位圖中第一個(gè)像素?cái)?shù)據(jù)的地址。它也可以看成是位圖中的第一個(gè)掃描行。
Stride 獲取或設(shè)置 Bitmap 對(duì)象的跨距寬度(也稱(chēng)為掃描寬度)。
下面的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); } //或 Bitmap bitmap = new Bitmap("image.jpg"); BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); // 對(duì) BitmapData 進(jìn)行圖像處理操作 bitmap.UnlockBits(bitmapData);
上面的代碼演示了如何用數(shù)組的方式來(lái)訪(fǎng)問(wèn)一幅圖像,而不在使用低效的GetPixel()和SetPixel()。
IntPtr
使用 IntPtr 可以直接操作圖像的內(nèi)存數(shù)據(jù)。
通過(guò)獲取圖像的句柄(IntPtr),可以使用指針操作來(lái)訪(fǎng)問(wèn)和修改圖像的像素值。
這種方法需要更高級(jí)的編程技能和對(duì)內(nèi)存管理的了解。
Bitmap bitmap = new Bitmap("image.jpg"); IntPtr ptr = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); // 使用 IntPtr 進(jìn)行圖像處理操作 bitmap.UnlockBits(ptr);
unsafe代碼
而在實(shí)際中上面的做法仍然不能滿(mǎn)足我們的要求,圖像處理是一種運(yùn)算量比較大的操作,不同于我們寫(xiě)的一般的應(yīng)用程序。我們需要的是一種性能可以同C++程序相媲美的圖像處理程序。C++是怎么提高效率的呢,答曰:指針。幸運(yùn)的是.Net也允許我們使用指針,只能在非安全代碼塊中使用指針。何謂非安全代碼?
為了保持類(lèi)型安全,默認(rèn)情況下,C# 不支持指針運(yùn)算。不過(guò),通過(guò)使用 unsafe 關(guān)鍵字,可以定義可使用指針的不安全上下文。在公共語(yǔ)言運(yùn)行庫(kù) (CLR) 中,不安全代碼是指無(wú)法驗(yàn)證的代碼。C# 中的不安全代碼不一定是危險(xiǎn)的,只是其安全性無(wú)法由 CLR 進(jìn)行驗(yàn)證的代碼。因此,CLR 只對(duì)在完全受信任的程序集中的不安全代碼執(zhí)行操作。如果使用不安全代碼,由您負(fù)責(zé)確保您的代碼不會(huì)引起安全風(fēng)險(xiǎn)或指針錯(cuò)誤。不安全代碼具有下列屬性:
- 方法、類(lèi)型和可被定義為不安全的代碼塊。
- 在某些情況下,通過(guò)移除數(shù)組界限檢查,不安全代碼可提高應(yīng)用程序的性能。
- 當(dāng)調(diào)用需要指針的本機(jī)函數(shù)時(shí),需要使用不安全代碼。
- 使用不安全代碼將引起安全風(fēng)險(xiǎn)和穩(wěn)定性風(fēng)險(xiǎn)。
- 在 C# 中,為了編譯不安全代碼,必須用 /unsafe 編譯應(yīng)用程序。
正如《C#語(yǔ)言規(guī)范》中所說(shuō)無(wú)論從開(kāi)發(fā)人員還是從用戶(hù)角度來(lái)看,不安全代碼事實(shí)上都是一種“安全”功能。不安全代碼必須用修飾符 unsafe 明確地標(biāo)記,這樣開(kāi)發(fā)人員就不會(huì)誤用不安全功能,而執(zhí)行引擎將確保不會(huì)在不受信任的環(huán)境中執(zhí)行不安全代碼。
以下代碼演示如何借助BitmapData類(lèi)采用指針的方式來(lái)遍歷一幅圖像,這里的unsafe代碼塊中的代碼就是非安全代碼。
//創(chuàng)建圖像 Bitmap image = new Bitmap( "c:\\images\\image.gif" ); //獲取圖像的BitmapData對(duì)像 BitmapData data = image.LockBits( new Rectangle( 0 , 0 , image.Width , image.Height ) , ImageLockMode.ReadWrite , PixelFormat.Format24bppRgb ); //循環(huán)處理 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; } }
毫無(wú)疑問(wèn),采用這種方式是最快的,所以在實(shí)際工程中都是采用指針的方式來(lái)訪(fǎng)問(wèn)圖像像素的。
字節(jié)對(duì)齊問(wèn)題
上例中ptr += data.Stride - data.Width * 3,表示跨過(guò)無(wú)用的區(qū)域,其原因是圖像數(shù)據(jù)在內(nèi)存中存儲(chǔ)時(shí)是按4字節(jié)對(duì)齊的,具體解釋如下:
假設(shè)有一張圖片寬度為6,假設(shè)是Format24bppRgb格式的(每像素3字節(jié),在以下的討論中,除非特別說(shuō)明,否則Bitmap都被認(rèn)為是24位RGB)。顯然,每一行需要6*3=18個(gè)字節(jié)存儲(chǔ)。對(duì)于Bitmap就是如此。但對(duì)于BitmapData,雖然data.Width還是等于image.Width,但大概是出于顯示性能的考慮,每行的實(shí)際的字節(jié)數(shù)將變成大于等于它的那個(gè)離它最近的4的整倍數(shù),此時(shí)的實(shí)際字節(jié)數(shù)就是Stride。就此例而言,18不是4的整倍數(shù),而比18大的離18最近的4的倍數(shù)是20,所以這個(gè)data.Stride = 20。顯然,當(dāng)寬度本身就是4的倍數(shù)時(shí),data.Stride = image.Width * 3。
畫(huà)個(gè)圖可能更好理解。R、G、B 分別代表3個(gè)原色分量字節(jié),BGR就表示一個(gè)像素。為了看起來(lái)方便我在們每個(gè)像素之間插了個(gè)空格,實(shí)際上是沒(méi)有的。X表示補(bǔ)足4的倍數(shù)而自動(dòng)插入的字節(jié)。為了符合人類(lèi)的閱讀習(xí)慣我分行了,其實(shí)在計(jì)算機(jī)內(nèi)存中應(yīng)該看成連續(xù)的一大段。
|-------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個(gè)像素的第0個(gè)分量的地址,這個(gè)地址指向的是個(gè)byte類(lèi)型,所以當(dāng)時(shí)定義為byte* ptr。行掃描時(shí),在當(dāng)前指針位置(不妨看成當(dāng)前像素的第0個(gè)顏色分量)連續(xù)取出三個(gè)值(3個(gè)原色分量。注意,0 1 2代表的次序是B G R。在取指針指向的值時(shí),貌似p[n]和p += n再取p[0]是等價(jià)的),然后下移3個(gè)位置(ptr += 3,看成指到下一個(gè)像素的第0個(gè)顏色分量)。做過(guò)Bitmap.Width次操作后,就到達(dá)了Bitmap.Width * 3的位置,應(yīng)該要跳過(guò)圖中標(biāo)記為X的字節(jié)了(共有Stride - Width * 3個(gè)字節(jié)),代碼中就是 ptr += dataIn.Stride - dataIn.Width * 3。
總結(jié)
通過(guò)閱讀本文,相信你已經(jīng)對(duì)使用C#進(jìn)行圖像處理可能用到的幾種方法有了一個(gè)了解。至于采用哪種方式,取決于你的性能要求。其中第一種方式最優(yōu)雅;第三種方式最快,但不是安全代碼;第二種方式取了個(gè)折中,保證是安全代碼的同時(shí)又提高了效率。熟悉C/C++編程的人可能會(huì)比較偏向于第三種方式,我個(gè)人也比較喜歡第三種方式。
以上就是C#進(jìn)行圖像處理的常見(jiàn)方法(Bitmap,BitmapData,IntPtr)使用詳解的詳細(xì)內(nèi)容,更多關(guān)于C#圖像處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#SuperSocket的搭建并配置啟動(dòng)總結(jié)
在本篇文章里我們給大家總結(jié)了關(guān)于C#SuperSocket的搭建并配置啟動(dòng)的相關(guān)內(nèi)容,正在學(xué)習(xí)的朋友們跟著參考下。2019-05-05winform樹(shù)形菜單無(wú)限級(jí)分類(lèi)實(shí)例
本文介紹了“winform樹(shù)形菜單無(wú)限級(jí)分類(lèi)實(shí)例”,需要的朋友可以參考一下2013-03-03C#模擬鏈表數(shù)據(jù)結(jié)構(gòu)的實(shí)例解析
這篇文章主要介紹了C#模擬鏈表數(shù)據(jù)結(jié)構(gòu)的實(shí)例解析,包括隊(duì)雙向鏈表的模擬方法,例子中隊(duì)鏈表的操作也有很好的說(shuō)明,需要的朋友可以參考下2016-04-04解決C#獲取鼠標(biāo)相對(duì)當(dāng)前窗口坐標(biāo)的實(shí)現(xiàn)方法
本篇文章是對(duì)在C#中獲取鼠標(biāo)相對(duì)當(dāng)前窗口坐標(biāo)的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05基于C#實(shí)現(xiàn)簡(jiǎn)易的鍵盤(pán)記錄器
本文將利用C#語(yǔ)言和HOOK技術(shù)來(lái)做一個(gè)鍵盤(pán)記錄器,看看一天下來(lái),我們點(diǎn)擊了多少次鍵盤(pán),哪些鍵的使用頻率最高,感興趣的小伙伴可以嘗試一下2022-08-08C#通過(guò)html調(diào)用WinForm的方法
這篇文章主要介紹了C#通過(guò)html調(diào)用WinForm的方法,涉及html頁(yè)面中使用JavaScript訪(fǎng)問(wèn)C#的相關(guān)技巧,需要的朋友可以參考下2016-04-04C#實(shí)現(xiàn)Stripe支付的方法實(shí)踐
本文主要介紹了C#實(shí)現(xiàn)Stripe支付的方法實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02