Sunday, February 21, 2010

GetDIBits vs. Scanline vs. Pixels[] in Delphi Bitmaps

I've been always wanting to sit down one day and solve this problem; test the speed of manipulating pixels on a Bitmap using the three options:

- the convenient but understandably slow Pixels[],
- the "interesting" Scanline property and
- the Windows API GetDIBits and SetDIBits.

I have been traditionally using Pixels for quick work and the Get/SetDIBits for low-level pixel manipulation such as the filters used in ToolBox. Never really used the Scanline property in anger, I was always thinking, DIBits "had" to be quicker. Little I knew...

I have been experimenting with two tools that I've written and never released to the public domain. The first is the BMPCreator which I wrote in order to create 256-level grayscale bitmaps, the other is the IconCreator which uses a grayscale bitmap and applies color maps to it, giving you the ability to create different colored versions of the same icon (like in my database visualization tool VirtualTreeNavigator).

Yesterday I wrote a little test app to test the above techniques for speed. The drill is, get a 32-bit bitmap and change every pixel one-by-one to a certain color. Here's the results:

Pixels[] Property

Simple loop and access of the Pixels array. The code looks like this:


for y := 0 to bmp.Height-1 do
for x := 0 to bmp.Width-1 do
bmp.Canvas.Pixels[x,y] := color;

Scanline Property


Declare a pointer to an array of integers (32-bit number) get the scanline pointer for each line of the bitmap and use it as a normal integer array for each pixel. Code looks like this:



type
TIntegerArray = array[0..MaxInt div SizeOf(integer) - 1] of integer;
PIntegerArray = ^TIntegerArray;
var
scanLine : PIntegerArray;
...
for y := 0 to bmp.Height-1 do
begin
scanLine := bmp.ScanLine[y];
for x := 0 to bmp.Width-1 do
scanLine^[x] := color;
end;

I have tried a variation of the code above, where you get a pointer to the first scanline (last line for Windows Bitmaps), and do the arithmetic (y * bmp.Width + x) for yourself but it didn't work as nicely as the example above.

GetDIBits and SetDIBits

The idea here is that we allocate memory for a big enough buffer to hold the bitmap's pixels and use GetDIBits to read the picture
into our array. Then we use our array as a two-dimensional or one-dimensional array, where every bmp.Width pixels
there's a new scanline. The code looks like this:

var
p : pointer;
bufInt : PIntegerArray;
...

clBMPHelper.GetDIBitsFromBitmap32(bmp, bmp.Width, bmp.Height, p);
bufInt := p;

for y := 0 to bmp.Height-1 do
for x := 0 to bmp.Width-1 do
bufInt[y * bmp.Width + x] := color;

clBMPHelper.SetDIBitsToBitmap32(bmp, bmp.Width, bmp.Height, p);
FreeMem(p);

As one loop of the above is quite quick, I have run the test multiple times (1000x).
The GetDIBits/SetDIBits allocation and deallocation of memory however, meant that
I had the option of allocating the memory, then running the test 1000 times, then deallocating. The clBMPHelper methods look like this:


procedure GetDIBitsFromBitmap32 ( const bmp : TBitmap; const a_iWidth, a_iHeight : integer; var p : pointer );
var
bi : TBitmapInfo;
res : integer;
error : cardinal;

begin
FillChar (bi, SizeOf(bi), 0);
with bi.bmiHeader do
begin
biSize := SizeOf(bi.bmiHeader);
biWidth := a_iWidth;
biHeight := a_iHeight;
biPlanes := 1;
biBitCount := 32;
biCompression := BI_RGB;
end;

// Allocate the memory for the pointer
GetMem(p, a_iWidth * a_iHeight * 4);

// now get the bits
res := GetDIBits (bmp.Canvas.Handle, bmp.Handle, 0, bmp.Height, p, bi, DIB_RGB_COLORS);
if res = 0 then
begin
error := GetLastError;
raise Exception.Create('clBMPHelper.GetDIBitsFromBitmap32: Cannot get Bitmap data with GetDIBits'#13#10+
'Error '+IntToStr(error)+': '+GetErrorString(error));
end;
end;

procedure SetDIBitsToBitmap32 ( const bmp : TBitmap; const a_iWidth, a_iHeight : integer; var p : pointer );
var
bi : TBitmapInfo;
res : integer;
error : cardinal;

begin
FillChar (bi,SizeOf(bi),0);
with bi.bmiHeader do
begin
biSize := SizeOf(bi.bmiHeader);
biWidth := a_iWidth;
biHeight := a_iHeight;
biPlanes := 1;
biBitCount := 32;
biCompression := BI_RGB;
end;

// now set the bits
res := SetDIBits(bmp.Canvas.Handle, bmp.Handle, 0, a_iHeight, p, bi, DIB_RGB_COLORS);
if res = 0 then
begin
error := GetLastError;
raise Exception.Create('clBMPHelper.SetDIBitsToBitmap32: Cannot set Bitmap data with SetDIBits'#13#10+
'Error '+IntToStr(error)+': '+GetErrorString(error));
end;
end;

Results

Algorithm \ (Width x Height)100x100400x4001600x100100x1600
Pixels[]4,70081,30081,30081,200
Scanline31296218578
GetDIBits(*)1871,5931,6091,735
GetDIBits(**)47781781797

(*) Allocation, GetDIBits and SetDIBits/De-allocation of memory each time (1000x times)

(**) Allocation of memory and GetDIBits before the 1000x loop, SetDIBits and De-allocation at the end

Times in milliseconds.

Conclusion

The numbers above speak for themselves. The Scanline property is the best way to manipulate pixels on a bitmap. GetDIBits supposedly is the quickest option, but only if you keep a big enough array for the Bitmap's bits in memory and work there. If you do have a Bitmap already in memory though, and you need to keep it there, the most economical, practical and quick way of manipulating pixels is to use the Scanline property.

Back to my code now, to convert some of my trusted utils to use scan lines... :-)

5 comments:

James Brown said...

I came across you blog while Googling "Delphi ScanLine". I have a question for you.
I have a Delphi 7 application up and running that uses ScanLine. It works fine with huge BitMapped images but I need to move it to D2010. It does compile fine but the bitmapped image flashes like mad.
Have you attempted ScanLine tests under D2010?
Regards....... Jim
www.SETI.Net

Bruce said...

It's a pity you didn't take the trouble to find the reason for your problem with the pointer arithmetic. It's considerable faster than using the scanline.

Secondly, why use pf32Bit when the alpha channel is not used? Using pf24Bit will save you 25% of the space.

Bruce said...

For some of my exploring of Bitmap scanline using pf24Bit, see this link.

http://dl.dropbox.com/u/18869118/Articles/Programming/xdot/index.htm

I did eventually find the reason but not really a satisfactory solution.

Christian Cristofori said...

It seems to me that GetDIBits(**) has a different curve of increment than ScanLine, it would be nice to see if that continues for example with a 1600x1600 bitmap, maybe with greater bitmaps the GetDIBits will become faster than the ScanLine.

Anton said...

Actually, ScanLine calls GetDIBits internally, so it's just can't be faster