Delphi中的Scanline属性,Delphi图像处理入门

efg's计算机实验室图象处理技术报告
用Delphi的Scanline属性对像素操作
Earl F. Glynn(翻译:酷太阳工作组 hanzo)
初次出现在1998年5月,Delphi Developer,pp.1-5,9
扩展了包括所有关于像素点规格(PixelsFormats)的信息。
在此基础上修改和扩展的文档成为efg的计算机实验室的实验室报告。
目录
介绍
Scanline 和 PixelFormat
使用Scanline的附加例子
Scanline的滥用
优化
使用Scanline中的意外情况
在Delphi3中新出现的Scanline属性允许你对单个像素进行快速访问,但是在你能正确访问像素点以前,你必须知道你正在操作的PixelFormat的详细情况。在这个技术报告中,我们会证明如何用Scanline属性来操作多种图象处理中的像素的。当然还有一些其它的应用。本报告分三个主要的章节。首先,每个不同的PixelFormats用一些相关的例子来讨论;然后,通过基本用pf24bit的Scanlines的一些例子来描述关于Scanlines的理论知识。最后说一下使用Scanline的意外情况。

--------------------------------------------------------------------------------
介绍
Delphi1和Delphi2提供了Pixels属性,可以在画布(canvas)上直接访问像素。但是用这个属性来执行访问是很慢的。比如,表1列出了如何使用Pixels属性来把一个位图旋转90度。这种方法对于小的位图处理效果还是不错的,但是对于大一些的位图就无法接受了。然而,Pixels属性对于所有的像素大小(1位到24位),处理的方法都是一样的。
表 1. 用Canvas.Pixels属性来旋转一幅位图
WITH ImageFrom.Canvas.ClipRect DO
BEGIN
FOR i := Left TO Right DO
FOR j := Top TO Bottom DO
ImageTo.Canvas.Pixels[j,Right-i-1] := ImageFrom.Canvas.Pixels[i,j]
END;
在Delphi1和Delphi2中,可以替代Pixels属性的就是直接引用windowsAPI来访问像素点数据(例如getDIBits)。但是用windowsAPI的DIB(Device Independent Bitmap)来访问像素数据更为复杂。假如你使用DIB数据,你必须经常把它转换到TBitmap,并以Timage来显示。Delphi3中TBitmap的Scanline和Pixelformat属性就提供了一个更好的Pixels属性的代替。
(注释:英文中关键词的引用比较正规,因为我把它翻成了中文,所以一些引用就中文化,如果大家在我保留了大量关键英文词后还不能读懂或者有摸棱两可的地方,请参看原版英文)
Scanline和PixelFormat
Delphi3的Scanline包含了像素点的数据,但在访问像素数据以前,你必须知道在内存中PixelFormat属性对Scanline属性的设计。PixelFromat已在Graphics.pas中定义了,其中包括pfCustom, pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, 和pf32bit。
对于所有的PixelFormats,每个Scanline的数据都被垫在了最近的一个双字的两端。
pfCustom的位图
当Delphi不能确认一幅位图的Pixelformat时,它就假设格式是pfCustom。假如你试着用assign分配一个pfCustom格式的话,一个无效的抗议就会显示出来(我们经常看见的Delphi的出错窗口)。
pfDevice的位图
假如你建立了一个TBitmap并且没有登记Pixelformat,也没有登记句柄类型,一个用pfDevice的DDB(Device-Dependent Bitmap)就会被建立,句柄就会是bmDIB。如果你设置HandleTYPE:=bmDIB,或你指定任何一种不是pfDevice的PixelFormat,你会得到一个Device Independent Bitmap(DIB)类型。
pf1bit的位图
如果你只需要每个像素只有一位就可以了的话,你可以节省许多内存空间,但在Delphi中你如果要访问这样一个像素点,你必须要位操作了。在每个像素点只有一位信息的情况下,pf1bit的位图中通常出现的颜色就是黑色和白色。也就是说,它定义了一个只有两种颜色的调色板。
我们用在Delphi的SysUtils.pas unit里定义的TByteArray来访问一个pf1bit Scanline:
pByteArray = ^ByteArray;
TByteArray = ARRAY[0..32767] OF BYTE;
如果宽度是8的倍数的话,pf1bit Scanline的字节宽度是Bitmap.width DIV 8。如果不是8的倍数,用1+(Bitmap.width - 1) DIV 8来计算字节长度。
用SysUtils定义的TByteArray限制了一个pf1bit的位图不会大于32768*32768个像素,这样大的位图超过了现在windows系统有效的内存存储能力(一幅位图123MB,实在太大了),Borland的解决方法是用有范围检测的数组来实现的,范围检测要求没有的变化,所有我更加喜欢上面这种结构。
pf1bit实验室报告(已经翻译)证明了如何用pf1bit位图来建立和操作位图,包括设计的两色调色板。按钮上显示黑(0)、白(1)。条纹按钮是{CONTENT}0,$FF和(01010101)。当这些按钮被按下,每个Scanline的每个字节就被分配到这个标签所表明的颜色了。把这幅位图分配到ImageBits.Picture.Graphic就可以显示了。源代码如下所示:
表 2. Filling a pf1bit bitmap with a constant mask
procedure TFormPf1bit.ButtonTagFillClick(Sender: TObject);
VAR
Bitmap: TBitmap;
i : INTEGER;
j : INTEGER;
Row : pByteArray;
Value : BYTE;
begin
// Value = {CONTENT}0 = 00000000 binary for black
// Value = $FF = 11111111 binary for white
// Value = = 01010101 binary for black & white stripes
Value := (Sender AS TButton).Tag;
Bitmap := TBitmap.Create;
TRY
WITH Bitmap DO
BEGIN
Width := 32;
Height := 32;
// Unclear why this must follow width/height to work correctly.
// If PixelFormat precedes width/height, bitmap will always be black.
PixelFormat := pf1bit;
IF CheckBoxPalette.Checked
THEN Bitmap.Palette := GetTwoColorPalette
END;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
Row := pByteArray(Bitmap.Scanline[j]);
FOR i := 0 TO (Bitmap.Width DIV BitsPerPixel)-1 DO
BEGIN
Row[i] := Value
END
END;
ImageBits.Picture.Graphic := Bitmap
FINALLY
Bitmap.Free
END
end;
Danny Thorpe(Borland R & D)关于“不清楚为什么……”的内容就在表2中:
肤浅的回答就是当位图句柄建立的时候会有一系列的变化,也就是说当位图有了非零的长与宽后,它会建立一个句柄。在建立了位图的长与宽以后再设置PixelFormat会复制一份新的像素格式的句柄。在建立长与宽以前设置PixelFormat,则被设置的信息会被存储直到句柄被确切地建立为止。
如果单DIB图象代码在DIB结构中是为被初始化的话,那么就会有问题……SrcDIB.dsbm.bmBits = nil在内部CopyBitmap被调用从抓图中建立一个位图并不属于这种情况(源代码 handle = 0, SrcDIB uninitialized)。这将在Delphi6中修补好。
在pf1bit实验室报告(已经翻译)中指出了如何建立“g”和如何建立箭头标志的方法。常量的字节都被分配给了Scanlines。下面的16进制数和2进制数就是用来建立“g”位图的两个Scanlines:
{CONTENT}0,$fc,{CONTENT}f,$c0
7,$ff,f,$e0
00000000 11111100 00001111 11000000
00000111 11111111 00011111 11100000
你可以从ButtonGClick的内容里看见所有的细节内容。
Invert按钮执行的是位图数据一位一位改变。表三里显示了Scanline的操作过程:
表 3. 改变一个 pf1bit 位图
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
RowOut := pByteArray(Bitmap.Scanline[j]);
RowIn := pByteArray(ImageBits.Picture.Bitmap.Scanline[j]);
FOR i := 0 TO (Bitmap.Width DIV BitsPerPixel)-1 DO
BEGIN
RowOut[i] := NOT RowIn[i]
END
END;
pf1bit位图是有调色板的,详情请看pf1bit实验室报告。
pf4bit的位图
操作pf4bit的位图比较复杂。每个像素只占部分的字节。所以就像pf1bit位图一样,pf4bit位图也需要位操作。每个像素有4位,可以表示16种颜色。在标准的16色VGA还经常被使用的情况下,它的调色板可能会更加复杂一些。
从pf4bit的代码,我们可以简单了解一下pf4bit位图的操作是怎么样的。要想知道pf4bit位图如何从一个常量数组中定义出来,如何像画板刷一样地被使用,请参看位图刷实验室报告。如果想了解更加多的信息,你可以参考一下组合pf4bit位图实验室报告。组合就是告诉我们如何用2幅pf4bit位图来建立pf8bit或者pf24bit的位图。在下一章关于pf8bit位图中我们会介绍一下部分的例子。
pf8bit的位图
操作pf8bit位图是很容易的,因为他的每个像素正好占一字节,能够在TByteArray中直接访问。表4显示了用一组嵌套循环分配pf8bit位图像素值的方法。(表4中的代码是CycleColors实验室报告中的一部分,这会在后面的章节中讨论)
Listing 4. Assigning values to a pf8bit Bitmap using a TByteArray
VAR
i : INTEGER;
j : INTEGER;
Row: pByteArray
. . .
FOR j := 0 TO BitmapBase.Height-1 DO
BEGIN
Row := BitmapBase.ScanLine[j];
FOR i := 0 TO BitmapBase.Width-1 DO
BEGIN
Row[i] := <pixel value 0..255>; // assign palette index
END
END;
ImageShow.Picture.Graphic := BitmapBase;
虽然用Scanline来操作8位的位图是简单的。但是分配给Scanline的描述像素颜色的字节值却是间接的。Scanline的值表示的是调色板上颜色的索引。调色板上包含了确切的R,G,B色。
我曾经用过超过内存地址表达范围的pf8bit位图,但我通常用pf24bit位图,这样相对于操作复杂的windows调色板来说,我更容易控制显示的颜色。(看下面的例子)
表5显示了如何把pf4bit位图的Scanline值转换成pf8bit位图的Scanline的值。pf4bit位图转换成pf8bit位图时,pf4bit位图要有两个调色板,而如果建立这个pf8bit位图的联合调色板,请参考组合pf4bit位图实验室报告。
表 5. pf4bit的scanline转换成pf8bit的scanline
(这里忽略了调色板的内容)
// Given a pf4bit Bitmap, Bitmap4, transfer the Scanline data to a
// pf8bit Bitmap, Bitmap8.
VAR
Bitmap4: TBitmap; // pf4bit Bitmap
Bitmap8: TBitmap; // pf8bit Bitmap
i : INTEGER;
j : INTEGER;
Row4 : pByteArray; // pf4bit Scanline
Row8 : pByteArray; // pf8bit Scanline
...
Bitmap8 := TBitmap.Create;
TRY
Bitmap8.Width := Bitmap4.Width;
Bitmap8.Height := 2 * Bitmap4.Height;
Bitmap8.PixelFormat := pf8Bit;
// Copy pf4bit Scanlines to pf8bit Scanline
FOR j := 0 TO Bitmap4.Height-1 DO
BEGIN
Row4 := Bitmap4.Scanline[j]; // "input" Scanline
Row8 := Bitmap8.Scanline[j]; // "output" Scanline
// Width[Bytes] = Width[Pixels] / 2 for pf4bit Bitmap
// Assume Width is even number
FOR i := 0 TO (Bitmap4.Width DIV 2)-1 DO
BEGIN
Row8[2*i ] := Row4[i] DIV 16;
Row8[2*i+1] := Row4[i] MOD 16
END
END;
Image8.Picture.Graphic := Bitmap8;
FINALLY
Bitmap8.Free
END
还有一个例子:如何使用GetPaletteEntries和如何通过Scanline把pf8bit位图变换成pf24bit位图。这个例子很有建设性意义,它指出了把pf8bit位图变换成pf24bit位图只不过是把一个新的值分配给PixelFormat属性的关键。
在内存中的pf8bit位图。你可以在内存中建立一幅pf8bit位图和它的调色板,但你要显示出来的话,还要看显示器设置的模式如何。
如果你用高彩色(15或16位色)或真彩色(24或32位色)显示模式,那么你可以在一幅pf8bit位图中定义和看到所有的256色的调色板颜色,如图一所示:
图一:用高彩色显示pf8bit位图时,它的256个灰度级
(注意:最下面的一条可能在你的系统是真彩色的时候看不见)


但是在256色模式下显示同样的pf8bit位图时,会失去20中颜色,如图二所示:
图二:pf8bit位图在256色显示模式下的256个灰度级
(注意:windows取走了20个颜色)


windows会取走256种颜色中的20种来显示面板、按钮等。通常是调色板前面和后面各十个颜色。剩下的236种颜色可以被你使用。
图三:被windows预定的20种颜色。

你可以从这里下载D3/D4的pf8bit调色板例子,从这个例子中你可以得到以上的结论。在建立pf8bit位图和它的调色板时你要特别小心,你必须首先了解位图是否会在真彩色模式下显示。
要知道更多的信息,调色板,B章的结论,颜色,请去Delphi图形算法区域。
pf15bit和pf16bit的位图
不是所有的PixelFormats在每台机器上都是有效的。就像pf15bit位图在一些机器上会受影象装置/影象驱动器的限制。
BEGIN
bitmap := TBitmap.Create;
TRY
bitmap.Width := 32;
bitmap.Height := 32;
bitmap.PixelFormat := pf15Bit;
IF bitmap.PixelFormat <> pf15bit
THEN ShowMessage('Not pf15bit);
...

如果你对你的操作有什么怀疑,你可以证明一下PixelFormat是否被正确建立了。如果PixelFormat不被支持,它显然会被分配给pfcustom。请参看Robert Rossmair的新闻组邮件是如何解决这个问题的。
我们用pWordArray来访问pf15bit和pf16bit像素的Scanline,pWordArray已经在Sysutils中定义了。这种方式也限制了位图的大小不会超过16384*16384个像素,但这也难保会不会用劲系统资源。
这种方式下设计的位是这样的:
pf15bit:0rrrrr ggggg bbbbb
pf16bit:rrrrr gggggg bbbbb
pf15bit像素有5位红色,5位绿色和5位兰色。pf16bit在绿色中要比pf15bit多一位,因为人的眼睛对绿色敏感一些。表6显示了如何建立一个pf15bit的黄色位图。注意,黄色是r=31,g=31,b=0。
表 6. 建立一个黄色的pf15bit位图
VAR
Bitmap: TBitmap;
i : INTEGER;
j : INTEGER;
R : 0..31; // each RGB component has 5 bits
G : 0..31;
B : 0..31;
RGB : WORD;
Row : pWordArray; // from SysUtils
...
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := Image1.Width;
Bitmap.Height := Image1.Height;
Bitmap.PixelFormat := pf15bit;
R := 31; // Max Red
G := 31; // Max Green
B := 0; // No Blue
// "FillRect" using Scanline
// The bits in each pixel (WORD) are as follows:
// 0rrrrrgggggbbbbb. The high order bit is ignored.
RGB := (R SHL 10) OR (G SHL 5) OR B; // "yellow"
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
Row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
Row[i] := RGB
END;
Image1.Picture.Graphic := Bitmap
FINALLY
Bitmap.Free
END
你也可以看看pf16bit相同的例子。
当把pf15bit中每5位一色变成每8位一色时,多出来的那三位发生了什么变化呢,请看efg's的新闻组邮件。
pf24bit的位图
关于pf24bit位图,我定义了(我希望Borland也会这么做)下面的表7,其中的内容和TByteArray类型差不多
表7 TRGBTripleArray 的定义
CONST
PixelCountMax = 32768;
TYPE
pRGBTripleArray = ^TRGBTripleArray;
TRGBTripleArray = ARRAY[0..PixelCountMax-1] OF TRGBTriple;
用这么大的PixelCountMax有这么两个目的,一个是限制位图大小,一个是关闭对Scanline变量范围的检查。
当表7对像素范围索引值做了一些限制时(你可能会用到更小一些的PixelCounetMax),你可以用下面的定义,它更加简单一些:
TYPE TRGBTripleArray = Array [WORD] OF TRGBTriple;
Borland在Graphics.PAS中定义了TRGBTripleArray,但是这是一个字节的类型。
Danny Thorpe(Borland R & D)的评语:
这是因为这个数组是用来处理调色板的。它并非用来访问像素。你会注意到它在D1~D4中的改进,我已经在位图常规中去掉了许多临时内存的分配。在许多节约时间的方案中,有一种就是不分配大堆的临时内存给调色板。就这一点而言,我只是用了一个局部变量:256 RGBTriples,这个变量的大小由我的需要来决定。这样的方式会用掉700~1K的堆栈空间,但是堆栈在win32中是很廉价的,它会在几个时间片的时间里回收利用。 如果你用WORD来定义RGBTriples,那么上面说的效用就没有了,它不会堆栈,显然也比较昂贵。
因为字节能表示的范围是0~255,所以Borland公司定义的TRGBTripleArray甚至在一般的位图操作上都会出错。我定义的TRGBTripleArray就不会了。
Borland公司的定义如下:
pRGBTriple = ^TRGBTriple;
TRGBTriple =
PACKED RECORD
rgbtBlue : BYTE;
rgbtGreen: BYTE;
rgbtRed : BYTE;
END;
表8显示了如何用pf24bit数组来建立黄色的像素。注意:黄色的表示是r=255,g=255,b=0。
表8 建立一个黄色的pf24bit位图
VAR
i : INTEGER;
j : INTEGER;
Row: pRGBTripleArray;
...
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf24bit;
Bitmap.Width := ImageRGB.Width;
Bitmap.Height := ImageRGB.Height;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
Row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
WITH Row[i] DO
BEGIN
rgbtRed := 255; // yellow pixels
rgbtGreen := 255;
rgbtBlue := 0;
END
END
END;
// Display on screen
ImageRGB.Picture.Graphic := Bitmap;
FINALLY
Bitmap.Free
END
如何把pf24bit位图转换成pf15bit位图呢?让我们考虑一下最难的和最简单的两种方法是如何处理PixelFormat的吧。看表9。
表9 把pf24bit位图变换成pf15bit的位图
VAR
Bitmap15: TBitmap;
Bitmap : TBitmap;
i : INTEGER;
j : INTEGER;
Row15 : pWordArray;
Row24 : pRGBTripleArray;
...
Bitmap := TBitmap.Create;
TRY
Bitmap.LoadFromFile('N:ImagesFlowersTulip3.BMP');
// Convert pf24bit bitmap to pf15bit bitmap the "hard way"
Bitmap15 := TBitmap.Create;
TRY
Bitmap15.Width := Bitmap.Width;
Bitmap15.Height := Bitmap.Height;
Bitmap15.PixelFormat := pf15bit;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
Row15 := Bitmap15.Scanline[j];
Row24 := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
WITH Row24[i] DO
Row15[i] := (rgbtRed SHR 3) SHL 10 OR
(rgbtGreen SHR 3) SHL 5 OR
(rgbtBlue SHR 3)
END
END;
Bitmap15.SaveToFile('Tulip3-15A.BMP');
FINALLY
Bitmap15.Free
END;
// Convert pf24bit bitmap to pf15bit bitmap the "easy way"
ASSERT(Bitmap.PixelFormat = pf24bit); // verify pf24bit
Bitmap.PixelFormat := pf15bit; // Assignment results in conversion
Bitmap.SaveToFile('Tulip3-15B.BMP')
FINALLY
Bitmap.Free
END
// Tulip3-15A.BMP and Tulip3-15B.BMP should be identical bitmaps
在上面的例子中,两种位图在像素级上是相同的,但它们在CRC-32的值上有不同,所以文件也就完全不同了,可能一些头文件上的值也会有一点点不同。
其它的例子:
如何用数字数据来建立位图(D5)
efg的 新闻组邮件 关于如何通过把Scanline写到一个内存流(MemoryStream)里来倒置一幅位图,如何用倒置后的顺序来LOAD Scanline。
pf32bit的位图
类似TRGBTriple,我们定义一个TRGBQuadArray来操作pf32bit位图。请看表10。
表 10. TRGBQuadArray 的定义
CONST
PixelCountMax = 32768;
TYPE
pRGBQuadArray = ^TRGBQuadArray;
TRGBQuadArray = ARRAY[0..PixelCountMax-1] OF TRGBQuad;
消除了PixelCountMax以后,我们可以得到一个更简单的定义:
TYPE TRGBQuadArray = ARRAY[WORD] OF TRGBQuad;

Borland公司的定义(在windows unit里)在下面:
pRGBQuad = ^TRGBQuad;
TRGBQuad =
PACKED RECORD
rgbBlue : BYTE;
rgbGreen: BYTE;
rgbRed : BYTE;
rgbReserved: BYTE
END;
在颜色上,TRGBQuad和TRGBTriple是一样的。两者都用24位来记录颜色信息,8位红色,8位绿色,8位兰色。TRGBQuad有更大一些的空间区域——“alpha”通道(特别在MAC机器上)。alpha通道在某些应用程序上用来传送灰度掩码信息,但现在还没有alpha通道相关的定义。
Danny Thope(Borland R & D)的评语:
NT支持(近乎)任意范围的RGB位数。唯一的限制是每种颜色的元素必须是相邻的。用RGB掩码来建立DIB,这样LOAD起来就会很烦琐。举个例子,我曾用24位色来建立32bpp的DIBSection,然后用同样的像素内存来建立另一个DIBSection,但这次在alpha通道里定义像素,并且字节由32bpp数据来定。
要使用pf32bit的Scanline就相当于要像使用单值动态数组一样来使用TBitmap。你可以用位图的SaveToFile方法和LoadFromFile来保存和读取这样大的数组。这个数字性质的数组甚至可以用Timage空间来显示,虽然Timage用的是浮点数,这也不会影响图片的质量。用这种科学/工程计算的方法建立出来的图象可以显示出一些很有趣的图案。
在有限的测试中,用pf32bit的Scanlines要比pf24bit快5%。这就是32位属性的特点。所以用pf24bit Scanlines有一些优点,但是对于pf32bit来说,却没有那么多足够的空间来保存Scanlines的值。
Ken Florentino的例子是用汇编语言写出来的,也表达了pf32bit的思想。
efg的新闻组邮件关于“pf32bit的秘密”:“alpha 字节”在新的位图中是不确定的吗?
请查看IEEE的一个如何用pf32bit位图来保持和显示数组的例子Lyapnnov Exponents实验室报告。Fractals Show 2实验室报告显示了当数组是4字节这个念书整数时,如何使用pf32bit位图。
PixelFormat的变换
分配一个新的PixelFormat单元,然后从一种PixelFormat变换到另一种PixelFormat。如果从低位/像素变换到高位/像素,或者是牵连到调色板的两个PixelFormat之间的变换,这将是个很不错的方法。例如pf15bit到pf24bit或者pf24bit到pf15bit。但分配新的PixelFormat就不保证你会拥有一个正确的调色板了。
一副pf24bit位图可以有几千种颜色。如大狒狒是512*512=262,144个像素的位图,它有230,427中颜色呢!(显示图象效用能给出一副图象中颜色的数量,从显示器左下角可以看到)在256色(pf8bit)显示模式下,windows通常保留20色来显示按钮,面板,图表等,剩下只有236色给应用程序。要在这样的情况下显示大狒狒图片,就要用一个算法来从230,427种颜色中挑选236色来用了。
参看显示一个演示方法的实验室报告,你可以得到一个在256色模式下显示24位/像素图象的算法。这个例子没有做的就是在pf24bit的Scanline中取得TRGBTriple,还有没有在调色板中查找最相近的颜色。对调色板的索引就成为了pf8bit Scanline的像素。
如果你所有的pf24bit图象在256色模式下都正确显示,那么你太幸运了。
--------------------------------------------------------------------------------
其它关于Scanline的记录
问(Paul Nicholls的新闻组邮件):“如果我想把一些Scanline的指针保存在变量里,在什么时候我必须更新变量里保持指针有效呢?”
答(Steve Schafer的新闻组邮件):“我认为保存Scanline的指针不会有用的,可能你在某个版本的Delphi中的某个环境下会有效,可是在其它大多数环境下都不行的。”
“找到一个Scanline的指针,使用它,然后就不必去理会它了。”
--------------------------------------------------------------------------------
使用Scanline的附加例子
菊花程序
菊花程序用了表8的技术来建立pf24bit图象。在DrawDaisy方法内的细节显示了如何建立这幅图象。扼要地说,这这幅图象中的红与绿的位面(planes)包含了所有这些颜色的变化。兰色的位面(planes)只包含了菊花最亮的兰色值,你需要800*600、15位色或者更加高的环境来运行这个应用程序。
用15位色来显示一副pf24bit图象或质量更加高的土峡谷内是比较简单的,因为windows只有256种颜色或更低的调色板或显示模式。如果你只是用256种颜色来显示pf24bit位图的话,那么你对windows的调色板就太仁慈了。我接触过CreatePalette,RealizePalette,等等。虽说调用API会比较复杂,但是你只是用真彩或高彩来显示你的图片,那么这个世界就变的简单了。(为什么要花那么多时间来和你解释上面的方法呢?)
分离程序
分离程序是一个学习位面(planes)简单图象处理程序。分离程序读入Daisy.bmp或其它24位色BMP文件。按下左边的按钮,你就会得到红、绿和蓝色分离的效果了。
分离程序会保持原始的图象,并把它做为生成其它图象的基础。
当按下单色按钮时,红、绿和蓝就会被分配成同一个亮度,这个亮度也就是(R + B + G) / 3。产生的图象就成了黑白色的了。具体内容见表11。
表 11. MakeShadesofGrayImage from RGB Composite Image
PROCEDURE TFormSplit.MakeShadesOfGrayImage;
VAR
Gray : INTEGER;
i : INTEGER;
j : INTEGER;
rowRGB : pRGBTripleArray;
rowGray: pRGBTripleArray;
BEGIN
Screen.Cursor := crHourGlass;
TRY
FOR j := BitmapRGB.Height-1 DOWNTO 0 DO
BEGIN
rowRGB := BitmapRGB.Scanline[j];
rowGray := BitmapGray.Scanline[j];
FOR i := BitmapRGB.Width-1 DOWNTO 0 DO
BEGIN
// Intensity = (R + G + B) DIV 3
WITH rowRGB[i] DO
Gray := (rgbtRed + rgbtGreen + rgbtBlue) DIV 3;
WITH rowGray[i] DO
BEGIN
rgbtRed := Gray;
rgbtGreen := Gray;
rgbtBlue := Gray
END
END
END;
FINALLY
Screen.Cursor := crDefault
END
END;
要显示红色的位面,红色像素的值被分配到另一个叫BitmapR的位图中,绿色和兰色就被分配为0。按了程序里的单色按钮,红色像素就被分配给rgbtRed,rgbtBlue和rgbtGreen,灰色阴影度的亮度表也会改变。
这里还有其它一些比较好的建立灰色比例的方法。请参看复数实验室报告 它里面显示了交替改变两个“Y”值——改变颜色信息(YUV/YIQ)灰度比例的例子,这个例子的显示模式是黑白显示模式。
解释RGB和HSV之间的变换并不是本文的范围,不过你可以去HSV实验室报告里得到相关的信息。(译者:相关参考书籍在中国境内没有比较好的可以介绍,以后本站会负责编写和翻译一些,如果有兴趣,大家也可以去efg的网站订购,不过那是在美国……)
图象的旋转
就像先前讨论的那样,用canvas的pixels属性来旋转一副位图会很慢。用Scanline就会比较快一些。旋转Scanline实验室报告(已经翻译)显示,在P166机器上旋转一幅24位640*480位图只要1秒钟多一点就够了。像素*像素旋转的算法如下:(译者:不知道为什么这次更新的Scanline报告把这个算法给删了,不过我还是保留下来)
x' = x * cos(theta) - y * sin(theta)
y' = x * sin(theta) + y * cos(theta)
(x,y)----->(x',y')
旋转结束以后,我们还要把中心归位为原来的中心。
事实上,不是每一个像素都要旋转到新的位置上的,参看Flip/Reverse/Rotate(已经翻译),只有在旋转细线和字的时候才需要这样。
pf24bit位图中颜色循环
旧的VGA调色板方法对24位色图象是不起作用的。记住,windows只用256色或者更低的调色板。调色板是不在高彩色(15,16位色)或者真彩色(24位色)的显示模式中使用的。
当windows使用调色板时,你只有中间的236色可以使用,上下各10色已经被windows占用。虽然在24位色图中不用硬件支持而用颜色循环的方法来实现颜色的话会慢一些,但你可以用1280色(5*256)的调色板了。(颜色循环实验室报告)
FormCreate方法定义了ColorCyle ARRAY OF TRGBTriples的入口。先是256个红色,然后是256个绿色和256个兰色。然后的256个是类似“火焰风暴”的调色板(到efg的网站上可以DOWN到这“火焰风暴”个程序,很经典)。第五部分是灰色。
在开始循环颜色程序时,在P166机器的环境下程序要花90秒来建立图象。(算法和数学内容不在本文范围里讨论)当建立图片完成以后,程序上的checkbox的按钮就有效了。这些零碎的图象是pf8bit位图(建立的方法与表4基本相同)
按下循环色的按钮以后,程序就在ColorCyle中定义RGB和基色等。看下面的表12。用软件来实现颜色的分配,虽然有点慢,但的确给人印象很深。
表 12. Using IdleAction to Cycle Colors

PROCEDURE TFormColorCycle.IdleAction(Sender: TObject; VAR Done: BOOLEAN);
VAR
i : INTEGER;
index : INTEGER;
j : INTEGER;
RowIn : pByteArray;
RowRGB: pRGBTripleArray;
BEGIN
IF NOT CheckBoxCycle.Checked
THEN Done := TRUE
ELSE BEGIN
INC (CycleStart);
IF CycleStart >= ColorList.Count
THEN CycleStart := 0;
LabelCycle.Caption := IntToStr(CycleStart);
FOR j := 0 TO BitmapBase.Height-1 DO
BEGIN
RowIn := BitmapBase.ScanLine[j];
RowRGB := BitmapRGB.ScanLine[j];
FOR i := 0 TO BitmapBase.Width-1 DO
BEGIN
index := CycleStart + RowIn[i];
IF index >= ColorList.Count
THEN index := index - ColorList.Count;
RowRGB[i] := pRGBTriple(ColorList.Items[index])^
END
END;
ImageShow.Picture.Graphic := BitmapRGB;
Done := FALSE;
END
END {IdleAction};
用Scanline对图象重复抽样的例子(D3~D5)
收缩/翻转/颠倒/图象的例子(D3)
比较两幅位图的例子(D3~D5)
--------------------------------------------------------------------------------
Scanline的滥用
Danny Thorpe(Borland R & D)
许多人用起Scanline一点顾虑都没有,因为它的确比大多数其它的方法好。Scanline的主要优点是用最小的开销来让你直接访问像素。但这不是说它是最快的。许多显卡现在都具有很复杂的、有各式各样功能的引擎,在处理图象的光栅操作上要比主CPU快许多。CPU只能在同一时刻访问一个像素,而且这样的操作还必须受到内存和缓存的限制。如果图象结构在显存中,CPU还必须通过PCI或AGP总线来访问。显卡就没有这样的限制,可以直接访问显存存,同一时间处理多个像素。在写显存和把显存清零方面,显卡更有较高的优先权。我听说一些3D显卡能很快地为显存清零,他们事实上已不访问RAM了。
表3的例子是通过重做每一个像素来实现的。我很怀疑在很破旧的VGA卡上是否能够同样执行这样的程序,PatBlt(Bitmap.canvas.handle,0,0,Bitmpa.width,Bitmap.height,DSTINVERT)。表3作为教学的例子是不错的,但从Delphi相关于Canvas.Pixels[]的文档来看,表3的例子就不适合实际的工作。我们会采用更简单的代码。
表6中也存在同样的问题,当函数被描述为等同于"FillRect"时,我们知道直接使用"FillRect"会比它更快,所以表6只是一种建议,并不能在实际中应用。
同样地,不通过像素操作我们也能够合并有调色板的位图。步骤是先合并调色板,把这个新的调色板分配给目标位图,然后就可以把源位图转换成目标位图了。GDI和显示卡等硬件会是颜色符合你的要求的。
在D4中,把调色板的Handle分配给一个位图目标,位图的像素会重新矢量化成为在新的调色板中与原来颜色最接近的颜色。D3和早一些的版本就不会了。
--------------------------------------------------------------------------------
优化
1 使Scanline的访问最小化
每次调用Scanline都会有开销的。所以作为优化的技术,许多次人们在Delphi的新闻组上都提到了这个开销的问题。表13是个优化的例子。原本Scanline在图象的每一行都要被执行一次的,但经过特殊处理以后,在每次循环的开头,Scanline只被访问两次。用算法,指针可以代替调用Scanline的过程。见表13。
表 13. 最小化 Scanline 的访问

procedure TForm1.ButtonOptimizedClick(Sender: TObject);
VAR
Bitmap : TBitmap;
Delta : INTEGER;
i : INTEGER;
j : INTEGER;
k : INTEGER;
LoopCount : INTEGER;
row : pRGBTripleArray;
ScanlineBytes: INTEGER;
StartTime : DWORD; // Use DWORD to keep D3 and D4 happy
begin
LabelOptimized.Caption := '';
LoopCount := SpinEditTimes.Value;
StartTime := GetTickCount;
FOR k := 1 TO LoopCount DO
BEGIN
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf24bit;
Bitmap.Width := 640;
Bitmap.Height := 480;
row := Bitmap.Scanline[0];

FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
WITH row[i] DO
BEGIN
rgbtRed := k;
rgbtGreen := i MOD 256;
rgbtBlue := j MOD 256;
END
END;
INC(Integer(Row), ScanlineBytes);
END;
ImageOptimized.Picture.Graphic := Bitmap
FINALLY
Bitmap.Free
END
END;
Delta := GetTickCount - StartTime; // ms
LabelOptimized.Caption := IntToStr(Delta) + ' Total ms; ' +
Format('%.1f', [Delta / LoopCount]) + ' ms/bitmap';
end;
但这个技术真的有效吗?为了知道这个技术如何有效,我们用一个D3/D4的小程序来比较以下强制执行的Scanline技术和表13的Scanline技术的优劣。结果数据在下面:
Time[ms] (Mean ?Standard Deviation) Per 640-by-480 Bitmap
(based on 5 trials each of which created 100 bitmaps with the ScanlineTiming program)

CPU芯片速度[MHz] 强制 [ms/bitmap] 优化 [ms/bitmap] 节省 [ms/bitmap]
120 188.3 ?0.6 187.9 ?0.9 0.4
166 70.9 ?0.07 69.8 ?0.04 1.1
400 16.27 ?0.02 15.40 ?0.00 0.87
450 17.69 ?0.35 16.78 ?0.32 0.91
在我看来,这个算法是最好的了,其它的都一般般。每幅位图能够节省一毫秒是很少见的,这种成果足以成为它标志性的优点了。不过在更快一些的机器上,这种算法只能节约5%左右的时间。
Robert Lee在电子邮件中关于这种“优化”的看法:
这个技术可以改变你对位图操作的方法。比如,这个程序中有一半的时间是用来建立和释放位图的。如果我们讨论的位图是已经建立好的,那么使用这种技术我们可以省去10%的时间。当然,如果尺寸比现在讨论的位图更大一些或者更小一些,又或者我们操作不止一幅位图,那么这个技术的优势就更加明显了。
我还要说明一下这个技术的效果,通常情况下,这个技术可以节省5%的时间,不过,最好的情况下可能会是2~4倍,也就是节省10%~20%的时间。
[感谢Robert]
Danny Thorpe(Borland R & D)的看法:
大量调用Scanline[]在执行上是有冲突问题的:是的,的确有潜在的执行上的冲突。除非你在访问指针的时候调用GDIFlush,否则DIBSection在内存中的存放就不是紧密连续的(和最近的GDI操作会冲突)。Scanline[]必须调用GDIFlush才能保证缓冲里的内容是同步的。假如位图被GDI修改过,那么调用GDIFlush可以等待GDI的队列变空(也就是说保持同步)。如果没有修改操作的话,GDIFlush就能够立即回调操作,但是这样毕竟还是有多余了的调用开销。
还有,TBitmap.GetScanlines()会调用TBitmap.Changing,这将建立一个完全新的位图,如果以前的位图是被其它操作(包括多个Scanline)共享的,那么这份新的拷贝则不是共享的。这样的话,以前Scanline的操作就完全脱离了这个新的位图,就算原来的位图不是共享的,那么现在的操作也会相对于以前有那么一些细微的脱钩。
表13里的优化技术是我在写TJPEGImage class时写的。那个时候工作忙抽不开身,一些应该包括进去的好技术也没有写进去。用你自己定义的指针代替Scanline,然后用算法优化调用Scanline的开始部分,这样就可以减少或消除Scanline的调用,在上一个Scanline结束,下一个Scanline开始时也不要忘记用这样的算法。不过这样做有两个要注意的地方:1)正确处理Scanline的扫描线中的DWORD队列;2)物理方面上内存中Scanline的设计。当位图的第一行像素数据在内存中的开头属于第一字节,那么DIB是被规定为从上而下的顺序;如果第一行的数据在内存中的开头是属于最后一个字节的,然后逆方向递增,那么DIB是被规定为自下而上的方向。很奇怪,一般DIB都是以自下而上为主的,这很可能起源于OS/2操作系统。
从Scanline[1]的地址里减去Scanline[0]的地址,这样的技术可以很好地解决上面两个问题。这种方法可以给予Scanline之间以空间,也可以填充DWORD的空隙,还有,delta标志也可以暗示DIB在内存中的方向到底是自下而上还是自上而下的。这种方法在关键指针循环中一定是需要条件来判断的。同delta来作为循环递增指针的数据,那么你就会得到下一个Scanline,这个地址或许是现在Scanline的在下面的,或许是在上面的。
如果你有更好的方法或者能够指出我的程序的不足,请回馈我。(译者:同样也请回馈我。)
2 用汇编语言来访问Scanline
Paul Nicholls的关于用BASM来访问pf16bit的Scanline的新闻组邮件。
Robert Rossmair的关于计算一幅位图的颜色的新闻组邮件。
3 把未加工的数据复制到Scanline区域里
在borland.public.delphi.graphics里贴着Mikael Stalvik的帖子,上面说他想在内存中建立一个4字节整数的位图,然后用pf32bit的位图来填充。我做了两个例子给他,一个是优化过的,一个不是优化的。但是两个执行下来的效果差不多。
非优化的方法是用递增的方法填充Scanline(自上而下):
表 14. "Unoptimized" Way to Fill Scanlines from In-Memory Array of Integers

procedure TForm1.Button1Click(Sender: TObject);
TYPE
TRGBQuadArray = ARRAY[WORD] OF INTEGER;
pRGBQuadArray = ^TRGBQuadArray;
VAR
i,j : INTEGER;
Bitmap: TBitmap;
row : pRGBQuadArray;
Start : DWORD;
begin
Start := GetTickCount;
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf32bit;
Bitmap.Width := 640;
Bitmap.Height := 480;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
row[i] := i*j // something to make interesting bitmap
END
END;
Image1.Picture.Graphic := Bitmap;
ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
FINALLY
Bitmap.Free;
END
end;

优化的方法包括了内存中所有数据单一移动到Scanline的区域。[注意:这里必须有一些Scanline的填充操作——我认为我们必须确认每个Scanline的长度是不是8的倍数——这里忽略源代码。这里假设每个Scanline的长度是8的倍数,所以最后也没有必要做填充处理了。]
但什么是每个pf32bit Tbitmap的像素的内存地址呢?把下面的IF条件语句加到循环语句里来判断就可以了:
// Show "corner" addresses of pixels in bitmap
IF ((i=0) OR (i=Bitmap.Width-1)) AND
((j=0) OR (j=1) OR (j=Bitmap.Height-1))
THEN Memo1.Lines.Add('(' + IntToStr(i) + ', ' + IntToStr(j) + ') = ' +
InttoHex( Integer(@row[i]),8 ));
下面是测试640*480pf32bit位图的数据结果:
某个特定位图像素在内存中的地址
行 列 0 ... 639
0 74E600 ... 74EFFC
1 74DC00 ... 74E5FC
... ... ... ...
479 623000 ... 6239FC
注意最大和最小的地址值。位图像素是0~479,而内存地址是479~0,所以内存中的方向是从左到右,自下而上的。
但这不是位图在内存中唯一可能的布局。像Alex Denissov在新闻组中指出的,当BitmapInfo.bmiHeader.biHeight的值是负的时候,逻辑上(0,0)是左上角,如果是正的,那么是左下角。位图的(0,0)一般是左上角。
如何知道Scanline的地址值是递增还是递减,有一个很简单的方法:
ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(Bitmap.Scanline[0]);
位图中有多少字节呢?计算一下(74EFFC + 4) - 623000 = C000 = 1,228,800 = 4*640*480就可以得到想知道的数值了。
用移动的方法,我们要知道源地址和目的地址。目的地址是最后一行第0个像素的地址,看下表:
表15. "优化" 方法
procedure TForm1.Button1Click(Sender: TObject);
VAR
Bitmap: TBitmap;
x : ARRAY OF Integer;
i,j : INTEGER;
p : ^Integer;
Start : DWORD;
begin
SetLength(x, 640*480);
FOR j := 0 TO 479 DO
FOR i := 0 TO 639 DO
x[640*j + i] := i*j; // something interesting
Start := GetTickCount;
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf32bit;
Bitmap.Width := 640;
Bitmap.Height := 480;
p := Bitmap.Scanline[Bitmap.Height-1]; // starting address of "last" row
Move(x[0], p^, Bitmap.Width*Bitmap.Height*SizeOf(Integer));
Image1.Picture.Graphic := Bitmap;
ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
FINALLY
Bitmap.Free;
END
end;

GetTickCount用来测量两个例子有多有效。在PIII650机器上我无法举出一个有标志性意义的数字来区别这两个例子的优劣。
最后两个值得注意的地方:
1 上面两种方法的思路是不同的,有自上而下和自下而上的颠倒区别。
2 由“I*J”(像素坐标)计算出的未处理的二进制整数数据得出了一个有趣的位图。
--------------------------------------------------------------------------------
使用Scanline中无法预料的情况(D3~D4)
1 只在TBitmap.Assign后使用Scanline会破坏原来的位图(D3/D4中的问题,已经在D5中解决)
TBitmap.Assign相对于“浅度复制”(只复制指针)来说是一个“深度复制”(复制所有的数据)。事实上,根据相关的统计,当某一个“标志”性的事件发生的时候,也就是说当位图复制了一份独立的拷贝的时候,Assign就是一个浅度复制了。这种细微的地方很少被察觉。
在下面的表里,单个像素值从浅度复制到深度复制的变化不是BUG就是很差的一个算法。
BitmapAlign := TBitmap.Create;
TRY
// Assign doesn't directly make an independent copy of a bitmap.
// Problem affects Delphi 3 and 4.
BitmapAlign.Assign(Bitmaps[2]);
// Force BitmapAlign to be totally independent of Bitmaps[2]
// This is an unfortunate kludge needed if Scanline is the only
// method to access pixel data. Without this kludge, the
// original bitmap and "new" bitmap are operating on the same
// pixel data!!!
BitmapAlign.Canvas.Pixels[0,0] := Bitmaps[2].Canvas.Pixels[0,0];
<Scanline-only access of pixel data in BitmapAlign here>
FINALLY
BitmapAlign.Free
END
这里还有一些简单的代码说明了这个问题:ScreenBitmapAssignEnigma.PAS.
2 pf24bit Scanline分配秘密(一个很少见的Delphi优化BUG)
这个问题出现在D3和D4,但在D4升级版里修复了(Build 5.104)。感谢Marko Peric告诉我Delphi4的升级版里修复了这个问题。
下面把pf24bit 的一个Scanline分配给另一个Scanline的分配方法会引起访问冲突,如果Delphi企图优化的话:
VAR
BitmapA : TBitmap;
BitmapB : TBitmap;
i : INTEGER;
j : INTEGER;
RGBTriple: TRGBTriple;
rowInB : pRGBTripleArray;
rowInA : pRGBTripleArray;
// Define a pf24bit BitmapA
BitmapB := TBitmap.Create;
TRY
BitmapB.Width := BitmapA.Width;
BitmapB.Height := BitmapA.Height;
BitmapB.PixelFormat := pf24bit;
FOR j := 0 TO BitmapA.Height-1 DO
BEGIN
RowInA := BitmapA.ScanLine[j];
RowInB := BitmapB.Scanline[j];
FOR i := 0 TO BitmapA.Width-1 DO
BEGIN
RowInB[i] := RowInA[i]; // Access Violation
END
END;
下面的是“最佳”的修改方法,(在关掉优化以后,这个算法会用掉很多时间):
VAR
RGBTriple: TRGBTriple;
// Kludge workaround for best performance
RGBTriple := RowInA[i];
RowInB[i] := RGBTriple
请参看这个问题详细的内容。令人惊奇的是,Borland公司说这不是一个BUG。他们告诉我说:“我们郑重地认为你的BUG报告里所阐述的内容不是一个BUG,可能是错误地理解了这个功能的工作原理。” (译者:/ME 绝倒)
3 Scanline代码碎片引起内部错误 C1127(D4)和 C1141(D5)。Borland Bug Case 430492.
--------------------------------------------------------------------------------
结论
这份技术报告包括了许多如何用Delphi的Scanline和PixelFormat的属性来操作像素的例子,里面列举了许多的方法。许多图象处理和图形用Scanline以后可以简短而有效地完成。我希望其他人也能够发现Delphi RAD在图象处理和计算机图形中一些有用的功能。
--------------------------------------------------------------------------------
参考
使用Device-Independent Bitmaps(位图)和Palettes(调色板)
http://support.microsoft.com/support/kb/articles/q72/0/41.asp
例子:16和32位位图格式
http://support.microsoft.com/support/kb/articles/q94/3/26.asp
--------------------------------------------------------------------------------
感谢 Danny Thorpe, Senior Engineer, Delphi R&D, 和他极具价值的反馈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值