程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> GDI+中常見的幾個問題(4)

GDI+中常見的幾個問題(4)

編輯:關於.NET

5.讀圖是快了,處理怎麼還是慢?

GDI+的Bitmap類提供了兩個罪惡的函數GetPixel, SetPixel,用來獲取某個像素點的顏色 值。這個2個函數如果只調用一次兩次也就罷了,萬一我想把整張圖片加紅一點,用下面的代 碼,我估計你等到黃花菜都涼了,還沒有算完呢。 看看下面的代碼是怎麼寫的。

1 FileStream fs = new FileStream(image, FileMode.Open, FileAccess.Read);
2 Image img = Image.FromStream(fs, false, false);
3 Bitmap bmp = new Bitmap(img);
4 img.Dispose();
5 fs.Close();
6
7 for (int j = 0; j < bmp.Height; j++)
8 {
9      for (int i = 0; i < bmp.Width; i++)
10      {
11           Color color = bmp.GetPixel(i, j);
12           color = Color.FromArgb(color.R + 20, color.G, color.B);
13           bmp.SetPixel(i, j, color);
14       }
15 }

代碼邏輯很清楚,第1到第5行,寫得很好,用了我們在前幾節裡面的方法,讀圖速度飛快 且不鎖文件。當然如果不用覆蓋原始文件,不用復制都可以,速度就更快了。接下來我們對 圖像做一個循環,一行一行更新圖像的數據。殊不知GetPixel和SetPixel是GDI裡面耗費最大 的函數之一,此外bmp.Height和bmp.Width也是慢得夠嗆,如果處理一張500M像素的照片,您 可以去喝杯茶,睡一覺再回來了。

Bitmap有個方法叫LockBits,就是把圖像的內存區域根據格式鎖定,拿到那塊內存的首地 址。這樣就可以直接改寫這段內存了。這個方法的設計是挺好,可惜都是C++作為源泉來的, .NET Framework裡面根本就不推薦用指針,VB裡面根本也就沒有指針。最後設計了一個雞肋 的IntPtr,將就掉了這個問題。C#其實還好,可以用unsafe code,也算是間接用了指針, VB.NET就痛苦了,需要用Marshal.Copy把內容Copy到一個byte數組裡面,然後處理完了再 Copy回去。所以結論就是,要用GDI+做圖像處理,最好別用VB.NET,否則內存翻倍。

讓我們來看看快速的寫法,注意在編譯的時候加上unsafe開關,允許C#使用指針:

1 FileStream fs = new FileStream(image, FileMode.Open, FileAccess.Read);
2 Image img = Image.FromStream(fs, false, false);
3 Bitmap bmp = new Bitmap(img);
4 img.Dispose();
5 fs.Close();
6
7 int width = bmp.Width;
8 int height = bmp.Height;
9 BitmapData bmData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
10 byte* p = (byte*)bmData.Scan0;
11 int offset = bmData.Stride - width * 3; //only correct when PixelFormat is Format24bppRgb
12 
13 for (int j = 0; j < height; j++)
14 {
15       for (int i = 0; i < width; i++)
16      {
17            p[2] += 20; //should check boundary
18           p += 3;
19      }
20      p += offset;
21 }
22
23 bmp.UnlockBits(bmData);
24 bmp.Dispose();

1-5行一樣的,不多說了。第7,8行保存一個臨時變量,不要每次都調用,bmp.Width, bmp.Height。

第9行是把圖像內容鎖定到系統內存。這個函數有2個重載,第二種比較復雜,是把用戶內 存中的內容鎖定。這個以後再說。第一種,也就是我們現在使用的這種,是把圖像的內容根 據一定的格式放到內存裡面,這裡我們使用的是Format24bppRgb,也就是24位色。在這種格式 下3個字節表示一種顏色,也就是我們通常所知道的R,G,B, 所以每個字節表示顏色的一個分 量。

第10行用了一個指針,指向這段內存的首地址。 這樣我們可以直接來修改圖像的內存信 息。因為每個顏色分量都是一個字節,所以用byte的指針,如果是Format48bppRgb,每個分 量用2個字節,那就該用Short指針了。

第11行需要多說兩句,Stride是指圖像每一行需要占用的字節數。根據BMP格式的標准, Stride一定要是4的倍數。據個例子,一幅1024*768的24bppRgb的圖像,每行有效的像素信息 應該是1024*3 = 3072。因為已經是4的倍數,所以Stride就是3072。那麼如果這幅圖像是 35*30,那麼一行的有效像素信息是105,但是105不是4的倍數,所以填充空字節,Stride應 該是108。這一行計算出來的offset就是3。這裡再留個問題,如果是16色圖,也就是4位色, 一幅50*50的圖像,Stride又應該是多少呢?

第13-21行就是循環的處理。其中第17行需要注意以下。這句話其實是錯的,因為p是 byte,沒有符號,最大值是255, 加20可能會溢出。如果你不希望它溢出,那麼可以改成行1 ,如果希望溢出就是最大值,那麼可以用行2。

1 p[2] = checked((byte)(p[2] + 20));
2 p[2] = (byte)Math.Min (byte.MaxValue, p[2] + 20);

大多處情況下,圖像處理用的都是行2的做法,但是這樣的話性能損失還是比較厲害的, 需要計算一個最大值,還有一個類型的轉換,我還在研究有什麼更快的辦法。還有一點,BMP 圖像裡面用的是小數端的存儲方式(Little Endian),所以實際存儲的圖像順序是G,B,R,這裡 要把圖像的紅色分量增加一些,改變的是第三個字節而不是第一個。

最後兩行就不說了,處理完畢需要Unlock,bitmap必須被dispose掉。

用以上代碼進行圖像處理的速度已經飛快了,最慢的部分就是調用那個LockBits的函數, 這個速度基本上跟GDI是差不多的。但是如果你還是覺得慢,那還有以下2種辦法可以提高性 能,不過它已經遠遠超過GDI+的范疇了。

1.別用GDI+的方法讀圖像,自己把圖像文件讀出來。如果是BMP倒是好辦,要是用 JPG,TIF,PNG,GIF....如果你足夠牛,可以隨手寫個FFT或者DCT,那我倒是建議可以自己寫寫 看,把JPG解壓縮出來,自己修改修改再壓縮回去。要是PNG/GIF/GIF 98,就是那個會動的 GIF,我就無語了。GIF的標准是公開的,不過時人家的專利,你寫了是要付錢的。所以,還是 算了吧。自己讀BMP可以非常快地把圖像處理掉,連LockBits都不需要。

2.如果圖像超級大,你又用了後面的行2進行處理,你對性能的要求又無比苛刻,那還 是有條路可以走。直接用MMX/SSE/3D Now指令集,4個字節同時上。你去謝謝Intel/Amd給了 那麼好的指令集把,可惜C#你就別想了,沒有辦法直接用的。自己用C++調用MMX指令集寫個 Dll,然後用interop調用。Interop的過程當中也是有損失的,Managed的內存空間用C#裡面 的Marshal是需要被轉換的。所以如果真的要這樣做,我推薦使用C++.NET,兩塊東西都能寫。 這裡我就不給出用MMX/SSE的例子了。最後再提一句,MMX對於圖像處理也足夠了,SSE主要是 給浮點運算的,如果你有圖像渲染的那種,用SSE會快很多,那是圖形學的內容了,偶不懂, 不多說,怕被人用棒子打。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved