如何对24位位图使用ScanLine属性?
image-processing
4
0

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

参考资料:
Stack Overflow
收藏
评论
共 1 个回答
高赞 时间 活跃

1.简介

在这篇文章中,我将尝试说明ScanLine属性仅用于24位位图像素格式以及您是否实际需要使用它。首先看一下使此属性如此重要的原因。

2. ScanLine与否...?

您可以问自己为什么要使用这种棘手的技术,例如,当您仅可以使用Pixels访问位图的像素时,就好像使用ScanLine属性一样。答案是,即使在相对较小的像素区域上执行像素修改时,也存在明显的性能差异。

Pixels属性在内部使用Windows API函数GetPixelSetPixel来获取和设置设备上下文颜色值。 Pixels技术的性能不足在于,通常需要在修改像素颜色值之前先获取它们,这在内部意味着同时调用了上述两个Windows API函数。 ScanLine属性之所以能赢得这场比赛,是因为可以直接访问存储位图像素数据的内存。而且直接内存访问仅比两个Windows API函数调用快。

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

3.像素深处

3.1原始数据

位图的像素数据(现在将其称为原始数据 )可以想象成字节的一维数组,其中包含每个像素的颜色分量的强度值序列。位图中的每个像素都由固定的字节数组成,具体取决于所使用的像素格式。

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

24位位图的原始数据示例

3.2案例研究

假设您有一个3位x 2像素(宽3像素;高2像素)的24位位图,请牢记在心,因为我将尝试解释一些内部原理并在其上显示ScanLine属性用法的原理。它之所以如此之小,仅仅是因为内部需要深空查看(对于那些视野开阔的人来说,这里是png格式的绿色图像的示例↘ 在此处输入图片说明 ↙:-)

3.3像素组成

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

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

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

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

4.充满色彩

4.1。初始值

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

初始通道值

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

5. ScanLine的秘密生活

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

5.1。 ScanLine目的

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

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

使用不同参数的ScanLine调用

5.2。 ScanLine的优势

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

ScanLine行数组

好吧,对于特定行的每个像素,我们都有一个颜色强度数组。考虑这种数组的迭代;遍历此数组一个字节并仅调整像素的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.相关阅读

收藏
评论
新手导航
  • 社区规范
  • 提出问题
  • 进行投票
  • 个人资料
  • 优化问题
  • 回答问题

关于我们

常见问题

内容许可

联系我们

@2020 AskGo
京ICP备20001863号