35

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

4

1 回答 1

73

一、简介

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

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 位 3x2 像素(宽 3 像素;高 2 像素)的位图并将其牢记在心,因为我将尝试解释一些内部结构并展示其ScanLine属性使用原理。它是如此之小,只是因为内部深视图需要空间(对于那些有明亮视线的人来说,这里是 png 格式的此类图像的绿色示例 ↘ 在此处输入图像描述↙ :-)

3.3 像素构成

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

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

相同的另一个视图提供了以下图像。每个框代表我们想象中的位图的一个像素。在每个像素中,您可以从原始数据字节数组中看到其坐标和 3 字节组及其索引:

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

4. 与色彩共存

4.1。初始值

我们已经知道,我们虚构的 24 位位图中的像素由 3 个字节组成 - 每个颜色通道有 1 个字节。当您在想象中创建此位图时,所有像素中的所有这些字节都已违背您的意愿初始化为最大字节值 - 255。这意味着所有通道现在都具有最大颜色强度:

初始通道值

当我们看一下每个像素的这些初始通道值混合了哪种颜色时,我们会看到我们的位图是entirely white. 因此,当您在 Delphi 中创建 24 位位图时,它最初是白色的。好吧,默认情况下,白色将是每种像素格式的位图,但它们的初始原始数据字节值可能不同。

5. ScanLine的秘密生活

通过以上阅读,我希望您了解位图数据是如何存储在原始数据字节数组中的,以及这些数据是如何形成单个像素的。现在继续讨论ScanLine属性本身以及如何在直接原始数据处理中发挥作用。

5.1。扫描线用途

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

下图说明了我们想象的位图和我们ScanLine使用不同行索引的属性获得的指针:

具有不同参数的 ScanLine 调用

5.2. 扫描线优势

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

扫描线行数组

好吧,我们有一个特定行的每个像素的颜色强度数组。考虑这种数组的迭代;将这个数组循环一个字节并仅调整一个像素的 3 个颜色部分中的一个会不太舒服。更好的是循环遍历像素并在每次迭代时一次调整所有 3 个颜色字节 - 就像Pixels我们过去所做的那样。

5.3. 跳过像素

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

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

由于我试图容忍那些 Delphi 版本低于 2009 的人,并且因为它使代码在某种程度上更易于理解,所以我不会使用指针算法进行迭代,而是在以下示例中使用带有指向它的指针的固定长度数组(指针在下面的 Delphi 2009 中,算术的可读性会降低)。

所以,我们有TRGBTriple一个像素的结构,现在我们为行数组定义一个类型。这将简化位图行像素的迭代。这是我刚从 ShadowWnd.pas 单元借来的(无论如何,这是一个有趣的课程的所在地)。这里是:

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

如您所见,它的一行限制为 4096 像素,对于通常较宽的图像来说应该足够了。如果这对您来说还不够,只需增加上限。

6. 实践中的扫描线

6.1。将第二行设为黑色

让我们从第一个例子开始。我们将想象的位图具体化,将其设置为适当的宽度、高度和像素格式(或者,如果您愿意,可以设置位深度)。然后我们使用ScanLine行参数 1 来获取指向第二行的原始数据字节数组的指针。我们得到的指针我们将分配给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. 使用亮度的灰度位图

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

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.相关阅读

于 2012-11-27T11:40:30.090 回答