Delphi GDI+ 圖形處理(2)
探究Delphi的圖形處理 之五 -- 使用Canvas類繪圖 |
作者:何詠 釋出日期:(2005-4-12 21:03:21) |
使用Canvas類繪圖
我們知道,在Canvas類中可以完成各種繪圖操作。仔細觀察,會發現在Delphi提供的許多元件中,都有Canvas類。這是因為這些元件都繼承自TGraphicControl基類,這個基類就提供了Canvas類。
但我們並不滿足於直接使用它們的Canvas來繪圖,這是沒有效率的。因為TGraphicControl是一個視覺化控制元件,當我們在這些控制元件上繪圖時,繪製的圖形會即時地翻印到前臺(即使用者的螢幕)上,而很多時候,我們希望在繪圖結束後才將影象翻到前臺,這樣可以大大提高工作效率。這裡就使用到了一個緩衝的思想。即在記憶體中開一塊空間,在這塊空間上繪圖,繪圖完後,再將這塊空間中的影象翻印到前臺。
這裡,我們可以使用Delphi為我們提供的TBitmap(點陣圖)類。這個類也提供了Canvas類,我們同樣可以在這個Canvas類上繪圖。繪製完後,我們用 控制元件名.Canvas.Draw(0,0,Bitmap)把這個點陣圖翻到前臺。
下面的例子可以在PaintBox上繪製一個漸變顏色的矩形。
程式2.1
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TfrmMain = class(TForm) PaintBox1: TPaintBox; btnDraw: TButton; procedure btnDrawClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.dfm} procedure TfrmMain.btnDrawClick(Sender: TObject); var Bit:TBitmap; i:Integer; begin Bit := TBitmap.Create; try Bit.Height := 300; Bit.Width := 387; For i := 0 to 200 do begin Bit.Canvas.MoveTo(50,i+50); Bit.Canvas.Pen.Color := RGB(0,0,Round((1-(i)/200) * 255)); Bit.Canvas.LineTo(350,i+50); end; PaintBox1.Canvas.Draw(0,0,Bit); finally Bit.Free; end; end; end. |
程式的執行結果如下:
現在說明一下上面的程式。首先我們用 Bit:=TBitmap.Create;語句在記憶體中建立一個位圖物件。然後分別設定了點陣圖的高度和寬度。接下來使用了一個迴圈語句一行一行地畫出顏色不斷加深的線條。最後在PaintBox的Canvas中把這個點陣圖複製過去。
事實上,這個程式只使用了極少量的繪圖方法,並不需要建立點陣圖物件繪圖。本程式使用點陣圖只是為了說明的方便。
探究Delphi的圖形處理 之六 -- 使用ScanLine屬性進行高效的影象處理 |
作者:何詠 釋出日期:(2005-4-12 21:02:19) |
使用ScanLine屬性進行高效的影象處理
在上一節的例子中,我們使用了Bitmap類。
Bitmap是一個處理點陣圖影象的類。這個類允許你載入、建立和處理點陣圖影象。
Delphi的圖形處理,都是使用Bitmap類來完成的。當然,用Bitmap類來處理圖片並不意味著Delphi只能處理點陣圖影象,你可以用支援其他圖片格式的類將這些圖片載入,然後把它們轉為Bitmap格式,再使用Bitmap類進行處理,最後在把Bitmap格式轉換為想要輸出的格式即可。這在後面的章節中會詳細地講解。
上一節中,我們提到了Canvas的Pixels屬性,該屬性可以讀取和更改點陣圖中每一畫素的顏色值,這一功能在圖形處理非常有用。因為影象處理濾鏡就是通過讀取每一畫素的顏色值決定當前畫素新的顏色值,通過改變這些顏色值來實現各種效果。但是,通過實驗我發現,對於一個較大的影象,使用Pixels屬性是非常慢的。處理一幅800*600的影象竟然需要幾秒中的時間。
這是因為Pixels屬性的Read和write過程呼叫了GetPixel和SetPixe這兩個GDI繪圖函式。每次執行SetPixels和GetPixel,都進行了大量的重複運算,這樣只要影象越大,處理時間會成倍增長。使用Pixels屬性來處理圖片肯定不可行。然而我發現了一種新的東西來取代Pixels屬性,這就是Bitmap類的ScanLine屬性。
ScanLine屬性返回一個位圖中一行畫素的顏色值。而且ScanLine屬性在讀取圖片的時候使用了DIB(位)處理方法,這種處理方法比通常的SetPixel和GetPixel快得多。下面的實驗就展示了使用Pixel屬性和使用ScanLine屬性的速度差異。
這個實驗是分別使用Pixel屬性和ScanLine屬性把一個大小為600*450的圖片轉為灰度。把圖片轉為灰度的演算法在後面的章節中有具體的介紹,這裡不再解釋。以下是使用Pixel屬性完成任務的程式。
程式2.2
unit Unit1; {使用Pixels屬性將影象轉為灰度} interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TForm1 = class(TForm) PaintBox1: TPaintBox; btnConvert: TButton; lblTime: TLabel; procedure FormCreate(Sender: TObject); procedure btnConvertClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure PaintBox1Paint(Sender: TObject); private Procedure DecodeColor(Const Color : TColor; var R,G,B:Byte); public { Public declarations } end; var Form1: TForm1; Bit: TBitmap; implementation {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin Bit:=TBitmap.Create; Bit.LoadFromFile('Test.bmp'); end; procedure TForm1.btnConvertClick(Sender: TObject); var i , j :Integer;NewColor:Byte; R,G,B:Byte; C:TColor; T:LongInt; begin T:=GetTickCount; For i := 0 to bit.Width-1 do begin for j := 0 to bit.Height-1 do begin C:= Bit.Canvas.Pixels[i,j]; DecodeColor(C,R,G,B); NewColor := (R+G+B) Div 3; Bit.Canvas.Pixels[i,j] := RGB(NewColor,NewColor,NewColor); end; end; T := GetTickCount -t; LblTime.Caption := '用時:' + IntToStr(T) + 'ms'; PaintBox1.Canvas.StretchDraw(Rect(0,0,320,240),Bit); end; procedure TForm1.DecodeColor(const Color: TColor; var R, G, B: Byte); begin R := Color mod 256; G := (color Div 256) mod 256; B := Color Div 65536; end; procedure TForm1.FormDestroy(Sender: TObject); begin Bit.Free; end; procedure TForm1.PaintBox1Paint(Sender: TObject); begin PaintBox1.Canvas.StretchDraw(Rect(0,0,320,240),Bit); end; end. |
以下是使用ScanLine屬性的程式:
程式2.3
unit Unit1; {使用ScanLine屬性完成任務} interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TForm1 = class(TForm) PaintBox1: TPaintBox; lblTime: TLabel; btnConvert: TButton; procedure FormCreate(Sender: TObject); procedure PaintBox1Paint(Sender: TObject); procedure btnConvertClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; Bit : TBitmap; implementation {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin Bit := TBitmap.Create; Bit.LoadFromFile('Test.Bmp'); Bit.PixelFormat := pf24Bit; Bit.HandleType := bmDIB; end; procedure TForm1.PaintBox1Paint(Sender: TObject); begin PaintBox1.Canvas.StretchDraw(Rect(0,0,320,240),Bit); end; procedure TForm1.btnConvertClick(Sender: TObject); var CurLine : PByteArray; NewColor : Byte; i : Integer; j : Integer; m : Integer; t : LongInt; begin t:=GetTickCount; For i := 0 to Bit.Height-1 do begin CurLine := Bit.ScanLine[i]; for j := 0 to Bit.Width-1 do begin m := j *3; NewColor := (curLine[m]+CurLine[m+1]+CurLine[m+2])Div 3; CurLine[m] := NewColor; CurLine[m+1] := NewColor; CurLine[m+2] := NewColor; end; end; t:=GetTickCount-t; PaintBox1.Canvas.StretchDraw(Rect(0,0,320,240),Bit); lblTime.Caption := '用時:'+IntToStr(t) + 'ms'; end; end. |
下面就來比較一下它們的執行結果:
|
|
使用Pixels屬性的執行結果 |
使用ScanLine屬性的執行結果 |
現在,我們來詳細分析ScanLine屬性的具體用法。
ScanLine屬性是一個只讀屬性,它返回一個數組指標,存放當前Bitmap第i行的畫素顏色值。
陣列指標的型別可以是PByteArray(位元組陣列指標)或者^array of TRGBTriple(畫素顏色陣列指標)。我覺得使用PByteArray型別是最直接、最方便的,在本文中,我們都將使用PByteArray型別。PByteArray型別指向一個Byte型別的一維陣列。這個陣列的第j*3個值表示當前行第j個畫素顏色值的B分值(藍色分值),第j*3+1個值表示當前行第j個畫素顏色值的G分值(綠藍色分值), 第j*3+2個值表示當前行第j個畫素顏色值的R分值(紅藍色分值)。其中j∈[0,影象寬度-1]。
ScanLine[i]表示當前Bitmap第i行的畫素值。因此RGB(ScanLine[i][j*3+2],ScanLine[i][j*3+1], ScanLine[i][j*3])可以表示影象中點(j,i)的顏色值。不過在程式中,這樣寫是沒有效率的,我們為了獲取一個畫素就用了3次ScanLine,浪費了很多時間。下面的程式碼可以用ScanLine讀取整個Bitmap的畫素值:
程式2.4
Type TPixels = Array of Array of TRGBTriple; Procedure ReadPixel(Pic: Tbitmap; var tPix: TPixels); Var PixPtr:PbyteArray;i,j,m:Integer; begin SetLength(tPix,Pic.Width,Pic.Height); Pic.PixelFormat := pf24bit; Pic.HandleType:=bmDIB; For i :=0 to pic.Height-1 do begin PixPtr:=Pic.ScanLine[i]; for j:= 0 to pic.Width-1 do begin m := j*3; tPix[j,i].rgbtBlue:=PixPtr[m]; tPix[j,i].rgbtGreen := PixPtr[m+1]; tPix[j,i].rgbtRed := PixPtr[m+2]; end; end; end; |
在此說明一下上面的程式碼。首先定義TPixels為一個二維動態陣列,型別為TRGBTriple。TRGBTriple是一個記錄型別,它可以儲存一個畫素值的R、G、B分值。下面是TRGBTriple的原形宣告:
TRGBTriple = tagRGBTRIPLE; tagRGBTRIPLE = packed record rgbtBlue: Byte; rgbtGreen: Byte; rgbtRed: Byte; end; |
因此,TPixels型別就可以表示Bitmap中所有畫素的值。值得強調的是程式的第二行:
Pic.PixelFormat := pf24bit; |
這行程式碼的作用是把這個點陣圖轉為24位點陣圖格式。因為只有24位的點陣圖格式才符合上面所說的規則。如果沒有這行程式碼,當程式碰上非24位點陣圖的檔案時就不能正常執行。至於Pic.HandleType := bmDIB;這行程式碼是為了強制把當前Bitmap的操作方式轉化為DIB方式,這只是為了確保萬無一失。
那麼,既然ScanLine屬性是隻讀的,我們如何改變這些顏色值呢?我們知道,ScanLine屬性返回的是一個指標。既然是指標,我們就可以改變指標所指向的資料,通過這種方式就可以改變Bitmap中的顏色值了。下面的程式段演示瞭如何把一個TPixels變數寫到Bitmap中去。
程式2.5
Procedure WritePixel(Pic: TBitmap; tPix: TPixels); var PixPtr:PByteArray;i,j,m:Integer; begin pic.PixelFormat := pf24bit; pic.HandleType:=bmDIB; Pic.Height := High(tPix[0])+1; Pic.Width:= High(tPix)+1; For i :=0 to pic.Height-1 do begin PixPtr:=Pic.ScanLine[i]; for j:= 0 to pic.Width-1 do begin m := j*3; PixPtr[M] := tPix[j,i].rgbtBlue; PixPtr[m+1] := tPix[j,i].rgbtGreen; PixPtr[m+2] := tPix[j,i].rgbtRed; end; end; end; |
這樣,我們在圖形處理時,就可以先用ReadPixel過程把點陣圖讀到一個TPixels型別的變數中去,然後處理這個TPixels變數,處理完後,用WritePixel過程把這個變數寫到Bitmap中去,這就完成了修改過程。