一、简介
在这篇文章中我将尝试解释ScanLine属性用法仅适用于 24 位位图像素格式以及您是否确实需要使用它。首先来看看是什么让这个属性如此重要。
2. 是否使用 ScanLine...?
你可以问自己为什么要使用这样棘手的技术,比如使用ScanLine财产似乎是当你可以简单地使用Pixels访问您的位图像素。答案是,即使在相对较小的像素区域上执行像素修改,也会出现明显的性能差异。
The Pixels属性内部使用 Windows API 函数 -GetPixel and SetPixel,用于获取和设置设备上下文颜色值。性能不足Pixels技术是你通常需要在修改像素颜色值之前获取它们,这在内部意味着调用两个提到的 Windows API 函数。这ScanLine属性赢得了这场比赛,因为它提供了对存储位图像素数据的内存的直接访问。直接内存访问仅比两次 Windows API 函数调用快。
但是,这并不意味着Pixels属性完全不好,您应该在所有情况下避免使用它。例如,当您偶尔要修改几个像素(不是大区域)时,那么Pixels可能对你来说就足够了。但当您要操作像素区域时,请勿使用它。
3. 像素深处
3.1 原始数据
位图的像素数据(我们称之为raw data现在)您可以将其想象为一维字节数组,其中包含每个像素的颜色分量的强度值序列。位图中的每个像素都由固定数量的字节组成,具体取决于所使用的像素格式。
例如,24 位像素格式的每个颜色分量(红色、绿色和蓝色通道)都有 1 个字节。下图说明了如何想象raw data此类 24 位位图的字节数组。这里每个彩色矩形代表一个字节:
3.2 案例研究
Imagine you have a 24-bit bitmap 3x2 pixels (width 3px; height 2px) and keep it in your mind because I'll try to explain some internals and show a principle of ScanLine property usage on it. It is so small just because of space needed for a deep view inside (for those having a bright sight is a green example of such image in png format here ↘ ↙ :-)
3.3 像素构成
首先让我们看一下位图图像的像素数据在内部是如何存储的;看着那(这raw data。下图显示了raw data字节数组,您可以在其中看到我们的小位图的每个字节及其在该数组中的索引。您还可以注意到,3 个字节的组如何形成各个像素,以及这些像素位于位图上的哪些坐标上:
另一个视图提供了下图。每个框代表我们想象的位图的一个像素。每一个pixel你可以看到它的坐标和 3 个字节组及其索引raw data字节数组:
4. 与色彩共存
4.1.初始值
我们已经知道,假想的 24 位位图中的像素由 3 个字节组成 - 每个颜色通道 1 个字节。当您在想象中创建此位图时,所有像素中的所有字节都已违背您的意愿初始化为最大字节值 - 255。这意味着所有通道现在都具有最大颜色强度:
当我们查看每个像素的这些初始通道值混合哪种颜色时,我们会看到我们的位图是entirely white。因此,当您在 Delphi 中创建 24 位位图时,它最初是白色的。好吧,默认情况下,白色将是每个像素格式的位图,但它们的初始值可能有所不同raw data字节值。
5. ScanLine的秘密生活
通过上面的阅读,我希望您了解位图数据是如何存储在raw data字节数组以及如何从这些数据形成各个像素。现在继续ScanLine财产本身以及如何直接发挥作用raw data加工。
5.1. ScanLine 目的
这篇文章的主菜是ScanLine属性,是一个只读索引属性,返回指向数组第一个字节的指针raw data属于位图中指定行的字节。换句话说,我们请求访问数组raw data给定行的字节数,我们收到的是指向该数组第一个字节的指针。该属性的索引参数指定我们想要获取这些数据的行的从 0 开始的索引。
下图说明了我们想象的位图和我们通过ScanLine使用不同行索引的属性:
5.2. ScanLine优势
因此,根据我们所知,我们可以总结一下ScanLine给我们一个指向某个行数据字节数组的指针。并用该行数组raw data我们可以工作 - 我们可以读取或覆盖它的字节,但只能在特定行的数组边界范围内:
好吧,我们有一个特定行的每个像素的颜色强度数组。考虑这样的数组的迭代;按一个字节循环遍历该数组并仅调整像素的 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. ScanLine 实践
6.1.将第二行设为黑色
让我们从第一个例子开始。我们将虚构的位图具体化,将其设置为适当的宽度、高度和像素格式(或者,如果您需要,也可以设置位深度)。然后我们使用ScanLine使用行参数 1 获取指向第二行的指针raw data字节数组。我们得到的指针将分配给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.使用亮度的灰度位图
作为一个有意义的示例,我在这里发布了使用亮度对位图进行灰度化的过程。它使用从上到下的所有位图行的迭代。然后为每一行获得指向raw data和以前一样作为像素数组。然后通过以下公式计算该阵列的每个像素的亮度值:
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 属性操作像素