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

TLa*_*ama 32 delphi image-processing

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

TLa*_*ama 67

1.简介

在这篇文章中,我将尝试ScanLine仅针对24位位图像素格式解释属性用法,如果您确实需要使用它.首先来看看是什么让这个属性如此重要.

2. ScanLine与否......?

您可以问自己为什么要使用这种棘手的技术,比如使用ScanLine属性,当您可以简单地使用Pixels访问位图的像素时.答案是即使在相对较小的像素区域上执行像素修改时也会出现明显的性能差异.

Pixels属性在内部使用Windows API函数 - GetPixel以及SetPixel用于获取和设置设备上下文颜色值.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的用途

该帖子的主要内容是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;
Run Code Online (Sandbox Code Playgroud)

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

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

type
  PRGBTripleArray = ^TRGBTripleArray;
  TRGBTripleArray = array[0..4095] of TRGBTriple;
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,它对一行的限制为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;
Run Code Online (Sandbox Code Playgroud)

6.2.使用亮度的灰度位图

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

Luminance = 0.299 R + 0.587 G + 0.114 B
Run Code Online (Sandbox Code Playgroud)

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

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;
Run Code Online (Sandbox Code Playgroud)

并可能使用上述程序.请注意,您只能将此过程用于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;
Run Code Online (Sandbox Code Playgroud)

7.相关阅读

  • 如果您有任何建议或需要纠正的问题,请随时发表评论或仅修改帖子.这只是初始版本,我希望它会增长.至少我想稍微修改插图(因为他们是我的:-)并专注于更多更好的例子. (2认同)
  • 由于您展示了内存布局,您可能希望扩展我们想要工作的原因*"仅在特定行的数组范围的范围内"*,即实际的内存布局.3x2样本的Fi为什么`Bitmap.Scanline [1] - Bitmap.Scanline [0]`不是'9'而是'12'(实际上大多数时候是'-12').(downvote不是我的..) (2认同)