如何使用24位位图的ScanLine属性?

如何使用ScanLine属性进行24位位图像素操作? 为什么我更喜欢使用它而不是经常使用Pixels属性?

1.介绍

在这篇文章中,我将尝试仅解释24位位图像素格式的ScanLine属性用法,以及是否真的需要使用它。 首先看看是什么让这个属性如此重要。

2. ScanLine或不… …?

你可以问自己,为什么要像使用ScanLine属性那样使用如此棘手的技术,看起来就是当你可以简单地使用Pixels来访问你的位图的像素。 答案是,即使在相对较小的像素区域上执行像素修改,也会引起显着的性能差异。

Pixels属性在内部使用Windows API函数 – GetPixelSetPixel来获取和设置设备上下文颜色值。 Pixels技术的性能缺点是,在修改它们之前,通常需要获取像素颜色值,内部意味着调用这两种Windows API函数。 ScanLine属性赢得了这场比赛,因为提供了直接访问存储位图像素数据的内存。 直接内存访问只是比两个Windows API函数调用更快。

但是,这并不意味着Pixels属性是完全不好的,你应该避免在所有情况下使用它。 如果偶尔修改几个像素(不是很大的区域),那么Pixels可能就足够了。 但是,当您要使用像素区域进行操作时,请不要使用它。

3.像素深处

3.1原始数据

一个位图的像素数据(我们现在称之为原始数据 )可以设想为一个单一的字节数组,包含每个像素的颜色分量强度值序列。 位图中的每个像素都由固定的字节数组成,这取决于使用的像素格式。

例如,24位像素格式的每个颜色分量都有1个字节 – 对于红色,绿色和蓝色通道。 下图说明了如何设想这种24位位图的原始数据字节数组。 这里的每个彩色矩形代表一个字节:

24位位图的原始数据示例

3.2案例研究

想象一下,你有一个24位的位图3×2像素(宽3px;高2px),并将其放在你的脑海中,因为我将尝试解释一些内部结构,并在其上显示ScanLine属性的使用原理。 因为内部深处所需要的空间太小了(对于有明亮景象的人来说,这里是一个png格式的绿色例子↘ 在这里输入图像描述 ↙:-)

3.3像素组成

首先让我们看看我们的位图图像的像素数据是如何在内部存储的; 看看原始数据 。 下图显示了原始数据字节数组,在这里你可以看到我们的微小位图的每个字节,其索引在该数组中。 您还可以注意到,3个字节的组是如何形成单个像素的,这些像素位于我们的位图上的坐标上:

案例研究位图的原始数据数组

另一个相同的观点提供了以下图像。 每个盒子代表我们虚构的位图的一个像素。 在每个像素中,您可以从原始数据字节数组中查看其坐标和3个字节的组以及它们的索引:

案例研究位图的原始像素图

4.与颜色一起生活

4.1。 初始值

正如我们已经知道的,我们假想的24位位图中的像素由3个字节组成 – 每个颜色通道1个字节。 当你在想象中创build这个位图时,所有像素中的所有字节都已经被初始化为最大字节值(已经被初始化为255)。这意味着所有通道现在具有最大颜色强度:

初始通道值

当我们看一下,从每个像素的这些初始通道值混合哪种颜色,我们会看到,我们的位图是entirely white 。 所以,当你在Delphi中创build一个24位的位图时,它最初是白色的。 那么,默认情况下,白色将是每个像素格式的位图,但是它们在初始的原始数据字节值中可能不同。

5. ScanLine的秘密生活

从上面的阅读中,我希望你能理解,位图数据是如何存储在原始数据字节数组中的,以及如何从这些数据形成单独的像素。 现在转到ScanLine属性本身,以及如何在直接原始数据处理中有用。

5.1。 ScanLine的目的

这个post的主菜, ScanLine属性是一个只读索引属性,它返回指向属于位图中指定行的原始数据字节数组的第一个字节的指针。 换句话说,我们请求访问给定行的原始数据字节数组,并且我们收到的是指向该数组的第一个字节的指针。 此属性的index参数指定我们想要获取这些数据的行的基于0的索引。

下图显示了我们的虚构位图以及ScanLine属性使用不同行索引得到的指针:

ScanLine调用不同的参数

5.2。 ScanLine的优势

所以,据我们所知,我们可以总结一下, ScanLine给了我们一个指向某个行数据字节数组的指针。 使用原始数据的行数组我们可以工作 – 我们可以读取或覆盖其字节,但只能在特定行的数组范围内:

ScanLine行数组

那么,我们有一个特定行的每个像素的颜色强度arrays。 考虑到这样的数组的迭代; 以一个字节循环该数组并且仅调整像素的三个颜色部分中的一个是不太舒服的。 更好的方法是循环遍历像素,并在每次迭代中一次调整所有3个颜色的字节 – 就像我们以前用的Pixels一样。

5.3。 跳过像素

为了简化行数组循环,我们需要一个匹配像素数据的结构。 幸运的是,对于24位位图,有RGBTRIPLE结构; 在delphi像TRGBTriple翻译。 总之这个结构看起来像这样(每个成员代表一个颜色通道的强度):

 type TRGBTriple = packed record rgbtBlue: Byte; rgbtGreen: Byte; rgbtRed: Byte; end; 

由于我试图容忍那些在2009年以下的Delphi版本,因为它使得代码在某种程度上更容易理解,所以我不会在迭代中使用指针algorithm,而是在下面的例子中使用一个指向它的固定长度数组(指针在下面的delphi2009年算术将不太可读)。

所以,我们有一个像素的TRGBTriple结构,现在我们为行数组定义一个types。 这将简化位图行像素的迭代。 这个我只是从ShadowWnd.pas单位借用的(反正一个有趣的类的家)。 这里是:

 type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple; 

正如你所看到的,它有一个行4096像素的限制,对于通常的图像应该足够了。 如果这对你来说还不够,只要增加上限即可。

6.实践中的ScanLine

6.1。 使第二行黑色

我们从第一个例子开始。 在这一点上,我们将我们的虚构的位图物化,设置适当的宽度,高度和像素格式(或者如果你想的话,有点深度)。 然后我们使用行参数1的ScanLine来获取指向第二行原始数据字节数组的指针。 我们得到的指针将分配给指向RowPixels数组的TRGBTriple ,因此从那时起,我们可以将它作为一个行像素数组。 然后我们遍历位图的整个宽度,并将每个像素的所有颜色值设置为0,这将导致位图的第一行为白色(白色是默认情况下,如上所述),什么使第二行黑色。 然后这个位图被保存到文件中,但是当你看到它时不会感到惊讶,它确实非常小:

 type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple; procedure TForm1.Button1Click(Sender: TObject); var I: Integer; Bitmap: TBitmap; Pixels: PRGBTripleArray; begin Bitmap := TBitmap.Create; try Bitmap.Width := 3; Bitmap.Height := 2; Bitmap.PixelFormat := pf24bit; // get pointer to the second row's raw data Pixels := Bitmap.ScanLine[1]; // iterate our row pixel data array in a whole width for I := 0 to Bitmap.Width - 1 do begin Pixels[I].rgbtBlue := 0; Pixels[I].rgbtGreen := 0; Pixels[I].rgbtRed := 0; end; Bitmap.SaveToFile('c:\Image.bmp'); finally Bitmap.Free; end; end; 

6.2。 使用亮度的灰度位图

作为一个有意义的例子,我在这里发布一个使用亮度灰度化位图的程序。 它使用从上到下的所有位图行的迭代。 然后为每一行获得一个指向原始数据的指针,并像以前那样作为像素数组。 对于该arrays的每个像素,然后通过以下公式计算亮度值:

 Luminance = 0.299 R + 0.587 G + 0.114 B 

然后将该亮度值分配给迭代像素的每个颜色分量:

 type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple; procedure GrayscaleBitmap(ABitmap: TBitmap); var X: Integer; Y: Integer; Gray: Byte; Pixels: PRGBTripleArray; begin // iterate bitmap from top to bottom to get access to each row's raw data for Y := 0 to ABitmap.Height - 1 do begin // get pointer to the currently iterated row's raw data Pixels := ABitmap.ScanLine[Y]; // iterate the row's pixels from left to right in the whole bitmap width for X := 0 to ABitmap.Width - 1 do begin // calculate luminance for the current pixel by the mentioned formula Gray := Round((0.299 * Pixels[X].rgbtRed) + (0.587 * Pixels[X].rgbtGreen) + (0.114 * Pixels[X].rgbtBlue)); // and assign the luminance to each color component of the current pixel Pixels[X].rgbtRed := Gray; Pixels[X].rgbtGreen := Gray; Pixels[X].rgbtBlue := Gray; end; end; end; 

以及上述程序的可能用法。 请注意,您只能对24位位图使用此过程:

 procedure TForm1.Button1Click(Sender: TObject); var Bitmap: TBitmap; begin Bitmap := TBitmap.Create; try Bitmap.LoadFromFile('c:\ColorImage.bmp'); if Bitmap.PixelFormat <> pf24bit then raise Exception.Create('Incorrect bit depth, bitmap must be 24-bit!'); GrayscaleBitmap(Bitmap); Bitmap.SaveToFile('c:\GrayscaleImage.bmp'); finally Bitmap.Free; end; end; 

7.相关阅读

  • Leonel Togniolli:如何使用扫描线
  • Earl F. Glynn:用Delphi的ScanLine属性操作像素