图像处理,是可视化编程的基础内容。
在Windows操作系统中,一切要输出到屏幕上的东西都是通过图形处理这部分的内容来实现的。
比如一个程序使用了标签控件,它看起来似乎并没有用到什么图形处理,但实际上标签控件就是通过使用GDI库中的图形处理函数来实现的。可见图形处理在编程中的重要性。
图像处理在实际的应用中也极具价值。
平面制作、动画制作等都离不开它。这一部分的内容十分繁多。我本次研究的内容,只是其中最基础的、最重要一部分。
探究Delphi的图形处理 之二 -- 基本图像处理函数 |
|
第二章 图像处理函数
2.1 为什么选择Delphi
所有的可视化编程语言都能够进行图像处理。但由于这些语言的定位不同,它们在进行图形处理的效率和便捷程度上也各不相同。
实际上,Visual C 的图像处理效率是最高的,Delphi已经把绝大多数GDI绘图函数都封装成可直接调用的类,使用它进行图形处理操作十分方便,而且Delphi 是Pascal演变而来的,Pascal具有严谨易读的特点,因此很容易上手。
2.2 Delphi中用于图形处理的类
Delphi为我们提供了许多图形图像方面的类,合理地使用这些类,我们可以方便地开发出各种图形处理程序。
这些类有TPicture、TBitmap、TGraphic、TIcon、TJPEGImage和TCanvas。其中,
TCanvas类用于绘图,
TPicture、TBitmap、TIcon和TJPEGImage是专门用来处理图片的类,
TGraphic是一个抽象类,一般不直接使用。
TPicture类可以载入所有支持的图片,
而TBitmap、TIcon、TJPEGImage分别用于处理各种类型的图片。
在实际应用中,我们一般用这些具体类型的类载入图片,再将图片转为Bitmap格式来处理。TPicture、TIcon、TJPEGImage类一般只用于输入和输出。例如,下面的代码可以载入一幅任意支持格式的图片(Delphi所支持的格式为bmp、jpg、dib、wmf和emf)。
Var Pic:TPicture;
Begin Pic := TPicture.Create; Pic.LoadFromFile(FileName); End;
用TPicture类来载入图片时,该类会根据文件名的扩展名来决定用何等方式来打开图片。
这就出现了一个问题,如果这个图片的扩展名被用户非法修改,程序就会把这个图片视为无效图片。在真正编程中,我们要用TPicture、TBitmap、TJPEGImage、TIcon依次尝试去打开图片。
另外,Delphi本身是不支持GIF文件格式的。我们可以借用一个第三方的类——GIFImage来让Delphi支持它。这个类在附带的光盘中可以找到。最终我们用下面的代码来完成载入图片的操作。
Procedure ReadPicture(FileName: String; Bitmap: Graphics.TBitmap); var pic:TPicture;Bit:Graphics.TBitmap;jpgPic:TJPEGImage;FGifPic:TGIFImage; icoPic:TIcon; begin FGifPic := TGifImage.Create; Pic:=Tpicture.Create; bit:=Graphics.TBitmap.Create; jpgPic:=TJPEGImage.Create; icoPic:=TIcon.Create; try pic.LoadFromFile(FileName); if uppercase(ExtractFileExt(Filename)) = '.ICO' then begin Bitmap.Height:=pic.Height; Bitmap.Width:=Pic.Width; Bitmap.Canvas.Draw(0,0,Pic.Graphic); end else bitmap.Assign(pic.Graphic); except try bitmap.LoadFromFile(FileName); except try jpgPic.LoadFromFile(FileName); bitmap.Assign(jpgPic); except try icoPic.LoadFromFile(Filename); bitmap.Free; Bitmap:=TBitmap.Create; bitmap.Height:=icoPic.Height; bitmap.Width:=icoPic.Width; bitmap.Canvas.Draw(0,0,icoPic); except try FgifPic.LoadFromFile(FileName); Bitmap.Assign(FGifPic.Bitmap); except bitmap.Height:=bitmap.Canvas.TextHeight('8'); bitmap.Width:=bitmap.Canvas.TextWidth('无效图片'); BitMap.Canvas.TextOut(0,0,'无效图片'); end;{try} end; end;{try} end;{try} end;{try} Pic.Free ; bit.Free; jpgPic.Free; icoPic.Free; FGifPic.Free; end; |
保存图片的方法跟打开图片的方法类似,我们可以使用不同类型的图片类的SaveToFile方法保存文件。下面的代码可以根据文件名中扩展名的不同使用不同的类来保存Bitmap。
Procedure SaveBitmap(FileName: String; PicB: TBitmap); var pic:TPicture; FileExt:String;picJPG:TJPEGImage;picGIF:TGIFImage; begin pic:=TPicture.Create; picJPG:=TJPEGImage.Create; picGIF:=TGIFImage.Create; try pic.Assign(PicB); FileExt:= ExtractFileEXT(FileName); if (Uppercase(FileExt)='.JPG')or(Uppercase(FileExt)='.JPEG') or(Uppercase(FileExt)='.JPE') then begin picJPG.Assign(PicB); picJPG.SaveToFile(FileName); end else If (UpperCase(FileExt)='.BMP')OR(UpperCase(FileExt)='.DIB')THEN Begin PicB.SaveToFile(FileName); end else if (UpperCase(FileExt)='.GIF') then begin picGIF.Assign(PicB); PicGIF.SaveToFile(FileName); end else Pic.SaveToFile(FileName); {End If} ShowPicture(PictureIndex,False,TPicture(PicB)); Finally pic.Free; picJPG.Free; PicGIF.Free; end; end; |
探究Delphi的图形处理 之三 -- GDI及Canvas类简介 |
作者:何咏 发布日期:(2005-4-12 21: |
2.3 GDI及Canvas类简介
GDI(Graphics Device Interface,图形设备接口)是Windows为我们提供的一个专门用于图形绘制和屏幕输出的类库。
这个类库提供了许多绘图函数,使用这些函数,我们几乎可以开发出所有的平面绘制、平面处理的程序。它同时也是Windows系统的核心,Windows系统中所有的绘图任务都由这个库来完成。在任何语言中,我们都可以调用这个库来完成绘图任务。
在Delphi中,我们已经有了一个已经封装了绝大多数GDI函数的类。使用这个类我们可以方便的完成各种图像处理任务。这就是我们要研究的Canvas类。
探究Delphi的图形处理 之四 -- Canvas类中的基本绘图方法 |
作者:何咏 发布日期:(2005-4-12 21:04:21) |
Canvas类中的基本绘图方法
Canvas就是“画布”的意思,使用Canvas类中的绘图方法,我们可以在这块画布上绘制各种图形。我们也可以通过设置每一个象素的颜色值来完成对图像的处理。下面列出了Canvas类中的一些常用绘图方法。
CopyRect(Dest:TRect;Canvas:TCanvas;Source:TRect);
此方法用于把Canvas所指定的画布的一部分(由Source指定)复制到当前画布中。Dest参数指定了复制后的图像在当前画布中的位置。例如下面的语句:
ThisCanvas.CopyRect(Rect(5,5,20,20),SourceCanvas,Rect(10,10,25,25));
可以把SourceCanvas中的(10,10,25,25)这一区域复制到ThisCanvas中的(5,5,20,20)区域中。
值得注意的是,TRect是一个Record类型的变量,用Rect的构造函数可以创建一个Rect变量。Rect类型所指定的区域是一个长方形,由(x1,y1,x2,y2)两个点来确定。例如,Rect(10,10,25,25)所确定的就是下图所示的区域:
Draw(x,y:Integer;Graphic:TGraphic);
此方法可以在当前画布中,以(x,y)为绘图原点绘制由Graphic所指定的图形或图片。
Ellips(x1,y1,x2,y2:Integer);
此方法可以在当前画布中,以(x1,y1),(x2,y2)两点所指定的矩形范围内绘制一个椭圆。并用画笔中所指定的颜色作为线条颜色,笔刷的颜色作为填充颜色。
MoveTo(x,y:Integer);
把画笔的位置移动到点(x,y)。
LineTo(x,y:Integer);
从画笔当前的位置绘制一条直线到点(x,y),并把画笔的位置移动到(x,y);
Polygon(Points:array of TPoint);
以Points中的点为顶点绘制一个多边形。并用画笔中所指定的颜色作为线条颜色,笔刷的颜色作为填充颜色。
StretchDraw(Const Rect:TRect;Graphic : TGraphic);
此方法可以在由Rect所指定的区域中绘制图片,图片会根据Rect的大小自动缩放。
Rectangle(x1,y1,x2,y2);
绘制由(x1,y1),(x2,y2)所确定的矩形。并用画笔中所指定的颜色作为线条颜色,笔刷的颜色作为填充颜色。
TextOut(x,y:Integer;Text:String);
以(x,y)为原点绘制参数Text所指定的文字。
TextHeight(Text:String);
返回在当前字体设置下,Text所指定的字符串的高度。
TextWidth(Text:String);
返回在当前字体设置下,Text所指定的字符串的宽高度。
除了这些基本绘图方法外,Canvas类中还有一些重要的属性,它们是:
Pen(画笔)
这个属性包含很多项目,其中Color指定了画笔的颜色,Weight指定了画笔的宽度,PenMode指定了画笔绘图的方式。
Brush(笔刷)
这个属性主要决定了图形的填充方式。Color指定了填充颜色,BrushStyle决定了填充方式。
Font(字体)
它决定了在Canvas中,使用TestOut命令画出的文字的字体和字号。
Pixels(象素数组)
这个数组包含了Canvas中每一个象素的颜色值。
在一般的编程中,我们在需要进行象素级的图像调整时,一般不使用Pixels属性。在Bitmap(位图)类中提供了一个ScanLine属性,使用它我们可以快速地进行象素读取和设置。这在后面的章节中有详细的说明。
Canvas类中所提供的绘图方法远远不止上面提到的这些,本文档所罗列的只是我认为最常用的方法,更多的信息可以参考Delphi的帮助系统。
探究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中去,这就完成了修改过程。