如何将ScanLine属性用于24位位图?

时间:2012-11-27 11:40:30

标签: delphi image-processing

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

1 个答案:

答案 0 :(得分:67)

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位位图设想原始数据字节数组。这里的每个彩色矩形代表一个字节:

Raw data example for 24-bit bitmap

3.2案例研究

想象一下,你有一个24位位图3x2像素(宽度3像素;高度2像素),并记住它,因为我会尝试解释一些内部结构并显示ScanLine属性使用的原则它。它是如此之小,仅仅因为内部深处需要的空间(对于那些有明亮视线的人来说,这是png格式的图像的绿色例子↘enter image description here↙: - )

3.3像素组成

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

Raw data array for the case study bitmap

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

Raw pixel illustration for the case study bitmap

4。与颜色共存

4.1。初始值

正如我们所知,我们想象的24位位图中的像素由3个字节组成 - 每个颜色通道1个字节。当你在想象中创建了这个位图时,所有像素中的所有字节都与你的意志相对应,最初的字节值被初始化为255.这意味着所有通道现在都具有最大颜色强度:

Initial channel values

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

5。 ScanLine的秘密生活

从上面的阅读中我希望您了解,位图数据如何存储在原始数据字节数组中,以及如何根据这些数据形成各个像素。现在转到ScanLine属性本身,以及如何在直接原始数据处理中发挥作用。

5.1。 ScanLine用途

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

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

ScanLine call with different parameters

5.2。 ScanLine优势

因此,根据我们所知,我们可以总结ScanLine为我们提供指向某个行数据字节数组的指针。使用原始数据的行数组,我们可以工作 - 我们可以读取或覆盖它的字节,但只能在特定行的数组范围的范围内:

ScanLine row array

嗯,我们为某一行的每个像素都有一系列颜色强度。考虑这种数组的迭代;将这个数组循环一个字节并仅调整一个像素的3个颜色部分中的一个,这不是很舒服。更好的是循环像素并在每次迭代时一次调整所有3个颜色字节 - 就像我们以前一样使用Pixels

5.3。跳过像素

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

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

由于我已经试图容忍那些在2009年以后使用Delphi版本的人,并且因为它使得代码更容易理解,所以我不会使用指针算法进行迭代,而是一个带有指针的固定长度数组它在以下示例中(指针算法在下面的Delphi 2009中可读性较差)。

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

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

正如您所看到的,一行的限制为4096像素,对于通常宽的图像应该足够了。如果这对您来说不够,只需增加上限即可。

6。 ScanLine在实践中

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