Dec 122019
 

As you will find in the documentation and on the web the usual way to access the pixels of a Bitmap in Delphi is using the Scanline[] array property. Something like this:

type
  TRgbTriple = packed record
    // do not change the order of the fields, do not add any fields
    Blue: Byte;
    Green: Byte;
    Red: Byte;
  end;

  TRgbTripleArray = packed array[0..MaxInt div SizeOf(TRgbTriple) - 1] of TRgbTriple;
  PRgbTripleArray = ^TRgbTripleArray;

procedure ConvertBmp(_InBmp, _OutBmp: TBitmap);
var
  x, y: Integer;
  Pixel: TRgbTriple;
begin
  Assert(_InBmp.PixelFormat = pf24bit);
  _OutBmp.SetSize(_InBmp.Width, _InBmp.Height);
  _OutBmp.PixelFormat := pf24bit;
  for y := 0 to Height - 1 do begin
    for x := 0 to Width - 1 do begin
      Pixel := PRgbTripleArray(_InBmp.Scanline[y])^[x];
      doSomething(Pixel);
      PRgbTripleArray(_OutBmp.Scanline[y])^[x] := Pixel;
    end;
  end;
end;

This code first checks that the input bitmap is using 24 bits per pixel, then sets the output bitmap to do the same. Then it enumerates through all the lines in the input bitmap and then all the pixels in that line, reads them does “something” with them and finally writes them to the corresponding pixel in the output bitmap.

Let’s assume a small VGA sized bitmap, so we get 480 lines with 640 pixels each. In total that makes 2 * 640 * 480 calls to ScanLine[], each taking a short time (There are multiple function calls in the getter method.).

Call that code for several bitmaps and you will end up with a huge amount of time spent in the calls to ScanLine[]. (Don’t just take my word for it, go ahead and time it!)

The first optimization that can be done is calling ScanLine[] only once for each line of each bitmap:

var
  InScanLine: PRgbTripleArray;
  OutScanLine: PRgbTripleArray;
// ...
  for y := 0 to Height - 1 do begin
    InScanLine := PRgbTripleArray(_InBmp.Scanline[y]);
    OutScanline := PRgbTripleArray(_OutBmp.Scanline[y]);
    for x := 0 to Width - 1 do begin
      Pixel := InScanLine^[x];
      doSomething(Pixel);
      OutScanLine^[x] := Pixel;
    end;
  end;

This reduces the number of calls to ScanLine by a factor of 640, which you will find is quite significant. (Again: Time it!)

But we still can do more. What if we only needed 4 calls in total rather than 2 * 480 ?

All we need to do is calculating the line addresses ourself. To do that we simply need two addresses, the one of the first and the one of the second line:

// if you are using Delphi 2007 or older you need to correct the NativeInt declaration from 8 bytes to 4 bytes:
{$IF SizeOf(Pointer) = 4}
type
  NativeInt = Integer;
{$IFEND}

function AddToPtr(const _Ptr: Pointer; _Offset: NativeInt): Pointer; inline;
begin
  Result := Pointer(NativeInt(_Ptr) + _Offset);
end;

function PtrDiff(const _Ptr1, _Ptr2: Pointer): NativeInt; inline;
begin
  Result := NativeInt(_Ptr1) - NativeInt(_Ptr2);
end;

var
  BytesPerPixel: NativeInt;
  InScanLine0: Pointer;
  InBytesPerLine: NativeInt;
  OutScanLine0: Pointer;
  InBytesPerLine: NativeInt;
  InPixel: PRgbTriple;
  OutPixel: PRgbTriple;
// ...
  BytesPerPixel := SizeOf(Pixel)  
  InScanLine0 := InBmp.ScanLine[0];
  InBytesPerLine := NativeInt(_InBmp.ScanLine[1]) - NativeInt(InScanLine0);
  OutScanLine0 := _OutputBmp.ScanLine[0];
  OutBytesPerLine := NativeInt(_OutBmp.ScanLine[1]) - NativeInt(OutScanLine0);
  OutPixel := OutScanLine0;
  for y := 0 to Height - 1 do begin
    for x := 0 to Width - 1 do begin
      InPixel := AddToPtr(InScanLine0, InBytesPerLine * y + x * BytesPerPixel);
      Pixel := InPixel^;
      doSomething(Pixel);
      OutPixel := AddToPtr(OutScanLine0, OutBytesPerLine * y + x * BytesPerPixel);
      OutPixel^ := Pixel;
    end;
  end;

What we do here is calculate the difference between the first two scan lines and use it to calculate the address of each scan line in the bitmap.

Note: Most of the time this difference will be negative because on Windows Bitmaps usually are stored bottom to top.

There are two inlined helper functions AddToPtr and PtrDiff who do the pointer arithmetic by converting the pointer to NativeInt and back.

Note that the NativeInt declaration in Delphi 2007 and older is wrong. That’s why we redeclare it as 4 bytes in the conditional define above if SizeOf(Pointer) is 4 bytes (32 bits). The code should also work for 64 bits, but I haven’t tried it.

 Posted by on 2019-12-12 at 18:34