【发布时间】:2012-11-27 11:40:30
【问题描述】:
【问题讨论】:
-
这个问题对这个网站来说不是一个很好的例子,我投票决定关闭它,因为它没有建设性。做同样的事情,但请不要删除它。我将扩展答案(并根据您的建议进行更正),也许还会修改问题以更好地适应这里,但这需要一些时间。
-
这个问题很好。无需关闭它。有人可能有更好的答案!并且总是有社区 wiki。
【问题讨论】:
在这篇文章中,我将尝试解释 ScanLine 属性的用法,该属性仅适用于 24 位位图像素格式以及您是否确实需要使用它。首先看看是什么让这个属性如此重要。
你可以问自己为什么要使用这种棘手的技术,比如使用ScanLine 属性,你可以简单地使用Pixels 来访问位图的像素。答案是即使在相对较小的像素区域上执行像素修改时,也会出现明显的性能差异。
Pixels 属性在内部使用 Windows API 函数 - GetPixel 和 SetPixel,用于获取和设置设备上下文颜色值。 Pixels 技术的性能不足是您通常需要在修改它们之前获取像素颜色值,这在内部意味着调用两个提到的 Windows API 函数。 ScanLine 属性赢得了这场比赛,因为它提供了对存储位图像素数据的内存的直接访问。而且直接内存访问比两个 Windows API 函数调用快。
但是,这并不意味着Pixels 属性完全不好,您应该避免在所有情况下使用它。例如,当您偶尔只修改几个像素(不是大区域)时,Pixels 可能对您来说就足够了。但是,当您要使用像素区域进行操作时,请不要使用它。
位图的像素数据(我们暂时称它们为原始数据)您可以将其想象为一维字节数组,其中包含每个像素的颜色分量的强度值序列。位图中的每个像素都由固定数量的字节组成,具体取决于使用的像素格式。
例如,24 位像素格式的每个颜色分量都有 1 个字节 - 用于红色、绿色和蓝色通道。下图说明了如何为这种 24 位位图想象 原始数据 字节数组。这里的每个彩色矩形代表一个字节:
假设您有一个 24 位位图 3x2 像素(宽 3 像素;高 2 像素)并将其牢记在心,因为我将尝试解释一些内部结构并在其上展示 ScanLine 属性使用的原理。它是如此之小,只是因为内部的深度视图需要空间(对于那些有明亮视线的人,这里是 png 格式的此类图像的绿色示例 ↘ ↙ :-)
首先让我们看看我们的位图图像的像素数据是如何在内部存储的;查看原始数据。下图显示了 原始数据 字节数组,您可以在其中看到我们的小位图的每个字节及其在该数组中的索引。您还可以注意到,3 个字节的组如何形成单个像素,以及这些像素位于我们的位图上的哪些坐标:
相同的另一个视图提供了以下图像。每个框代表我们想象中的位图的一个像素。在每个 pixel 中,您可以看到它的坐标和 3 字节组及其索引来自 原始数据 字节数组:
我们已经知道,我们虚构的 24 位位图中的像素由 3 个字节组成 - 每个颜色通道 1 个字节。当您在想象中创建此位图时,所有像素中的所有这些字节都已违背您的意愿初始化为最大字节值 - 255。这意味着所有通道现在都具有最大颜色强度:
当我们查看每个像素的这些初始通道值混合了哪种颜色时,我们会看到我们的位图是entirely white。因此,当您在 Delphi 中创建 24 位位图时,它最初是白色的。好吧,默认情况下,白色将是每个像素格式的位图,但它们的初始 原始数据字节值可能不同。
希望您从上面的阅读中明白,位图数据是如何存储在原始数据字节数组中的,以及这些数据是如何形成单个像素的。现在继续讨论ScanLine 属性本身,以及如何在直接原始数据处理中发挥作用。
这篇文章的主菜,ScanLine 属性是一个只读索引属性,它返回指向 原始数据 字节数组的第一个字节的指针,该字节数组属于位图。换句话说,我们请求访问给定行的原始数据字节数组,我们收到的是指向该数组第一个字节的指针。此属性的 index 参数指定我们要获取这些数据的行的基于 0 的索引。
下图说明了我们想象的位图以及我们使用不同行索引通过ScanLine 属性获得的指针:
所以,据我们所知,我们可以总结出ScanLine 给了我们一个指向某个行数据字节数组的指针。使用 原始数据 的行数组,我们可以工作 - 我们可以读取或覆盖它的字节,但只能在特定行的数组边界范围内:
嗯,我们有一个特定行的每个像素的颜色强度数组。考虑这种数组的迭代;将这个数组循环一个字节并仅调整一个像素的 3 个颜色部分中的一个会不太舒服。更好的是循环遍历像素并在每次迭代时一次调整所有 3 个颜色字节 - 就像我们过去那样使用 Pixels。
为了简化行数组循环,我们需要一个与像素数据相匹配的结构。幸运的是,对于 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 像素,对于通常较宽的图像来说应该足够了。如果这对您来说还不够,请增加上限。
让我们从第一个例子开始。我们将想象的位图具体化,将其设置为适当的宽度、高度和像素格式(或者,如果您愿意,可以设置位深度)。然后我们使用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;
作为一个有意义的例子,我在这里发布了一个使用亮度对位图进行灰度化的过程。它使用从上到下的所有位图行的迭代。然后为每一行获得指向 原始数据 的指针,并像以前一样将其作为像素数组。然后通过以下公式计算该数组的每个像素的亮度值:
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;
【讨论】:
Bitmap.Scanline[1] - Bitmap.Scanline[0] 不是“9”而是“12”(或者实际上大多数时候是“-12”)。 (downvote 不是我的..)
ScanLine 的用途。我忘了在 5.2 中注意到这一点。谢谢!