How to Use Scanlines

By: Leonel Togniolli

Abstract: This article shows a fast way to manipulate bitmaps, without using canvas : the scanlines.

How To Use Scanlines - By Leonel Togniolli

Scanlines are a very fast way to to access an image, while accessing directly the pixels is very slow. Its very easy to use it, and most of the code written using the pixels of a canvas can be easily changed to use scanlines, and routines planned to use them can have performance several times better.

I do not intend to replace Earl's excellent resource on scanlines , but to provide a simpler introduction to them, and let his article be a more advanced technical reading.

Scanlines are the horizontal rows of an image. One thing extremelly important is that they are PixelFormat dependant, meaning that their structure vary with the color depth of the image. One of the most common mistakes when learning is forgetting to set the PixelFormat properly before trying to access them. When you get a scanline, you get a pointer to the first byte of that row, and have access byte by byte. That means that the size of a scanline is * , padded to next dword.When working in pf32bit mode (4 bytes) - which we will assume from now on -, a scanline looks like this :

Blue Green Red Alpha Blue Green Red Alpha Blue Green Red Alpha ...
   first pixel    /   second pixel    /    third pixel    / 

Tip: Alpha is usually used to store information about transparency. In pratice, you can store whatever you need in the alpha channel.

But thats not a good way to work. We are going to define a type containing the four color components, and through good old pointer arithmetic, work our way every four bytes in the scanline. You could also use TRGBQuad here, the important is that you understand how it works.


type
  TRGB32 = packed record
    B, G, R, A: Byte;
  end;
  TRGB32Array = packed array[0..MaxInt div SizeOf(TRGB32)-1] of TRGB32; 
  PRGB32Array = ^TRGB32Array;

We have now defined a record containing the color components, a very big array of it, and a pointer to this array. Now we can access any byte in this row, using this definition as a mask to the array of bytes ScanLine[y] points to.This is how I did it:

Drop a TImage in a Form. Add a button with the following code in its OnClick handler :


procedure TForm1.Button1Click(Sender: TObject);
var 
  x,y : Integer;
begin
  with Image1.Picture.Bitmap do
  begin
    PixelFormat := pf32bit;
    Width := Image1.Width;
    Height := Image1.Height;
    for x := 0 to Width - 1 do
      for y := 0 to Height - 1 do
         Canvas.Pixels[x,y] := x xor y;
  end;
  Image1.Invalidate;
end;

That should draw a nice red pattern. Now we are going to do it using scanlines. Since we have access to an entire row at a time, it makes sense to iterate through them first. Add a second button and add this code to it :


procedure TForm1.Button2Click(Sender: TObject);
var 
  x,y  : Integer;
  Line : PRGB32Array;
begin
  with Image1.Picture.Bitmap do
  begin
    PixelFormat := pf32bit;
    Width := Image1.Width;
    Height := Image1.Height;
    for y := 0 to Height - 1 do
    begin
      Line := Scanline[y];
      for x := 0 to Width - 1 do
      begin
        Line[x].B := 0;
        Line[x].G := 0;
        Line[x].R := x xor y;
        Line[x].A := 0;
      end;
    end;
  end;
  Image1.Invalidate;
end;

This is how it should look:

Notice that you have now access to every component of the color of a given pixel, where every byte represents a color intensity from 0 to 255, instead of suppling a TColor like in the first routine. According to Delphis help, if you specify TColor as a specific 4-byte hexadecimal number instead of using the constants defined in the Graphics unit, the low three bytes represent RGB color intensities for blue, green, and red, respectively. The value $00FF0000 represents full-intensity, pure blue, $0000FF00 is pure green, and $000000FF is pure red. $00000000 is black and $00FFFFFF is white.. That means that you can get any color component of a given TColor if you AND it with the constants above, ie (clGray and $000000FF) gives you the red component of clGray.

I timed both routines, calling each one of them ten times in a 1024x1024 bitmap. Button1Click averaged 7298ms per call, while Button2Click averaged 24ms per call. Thats about 300 times faster using scanlines.

Remember that is very important that the PixelFormat is set to pf32bit, or you would have to use a diferent record to access them. The default value of this property is pfDevice, with is the current color depth of windows. So, if you want your code to work properly on every machine, remember to set it explicitely.

Scanlines are stored in memory sequencially, usually in reverse order, like this:

BGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGR(...)BGR(Padding) // Last Line
(...)
BGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGR(...)BGR(Padding) // Second Line
BGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGRBGR(...)BGR(Padding) // First Line

They are also equally long. We can use that in our favor. If we have a pointer to the first line, and the difference, in bytes, between each line, we can access any pixel by pure math. I declared variables to hold them in the private section of the form :


  private
    LineLength : Longint;
    FirstLine  : PRGB32Array;

Loaded then on the OnCreate method of the form :


procedure TForm1.FormCreate(Sender: TObject);
begin
  with Image1.Picture.Bitmap do
  begin
    PixelFormat := pf32bit;
    Width := Image1.Width;
    Height := Image1.Height;
    FirstLine := Scanline[0];
    LineLength := (Longint(Scanline[1]) - Longint(FirstLine)) div SizeOf(TRGB32);
  end;
end;

And used them on the OnMouseMove event of the image :


procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
  if (ssLeft in Shift) and (x in [0..Image1.Width-1]) and (y in [0..Image1.Height-1]) then
  begin
    with FirstLine[Y*LineLength+X] do
    begin
      B := 0;
      G := 0;
      R := 255;
      A := 0;
    end;
    Image1.Invalidate;
  end;
end;

Now click on the image puts and move your mouse around . It puts red dots on it (blazing fast! ). Try to access the pixel like the in last procedure, using with, because its slightly faster, since it only evaluates the expression and access the array once. You can check this on the CPU window, if you are just a bit proeficient in ASM.

You can download this sample application from CodeCentral.

You should make sure that you always have valid pointers to scanlines, so you should refresh your cache whenever the bitmap has changed Width, Heigth, PixelFormat, etc. A good place to do it is in the TBitmap's Changed virtual method, if you have a TBitmap descendant. Another thing you should pay special attention to is not to mix this cached scanline access and direct GDI access. If you do, make sure to call GDIFlush to make sure the changes are applied, or call ScanLine[x] again, since it makes sure everything is in order.

Thats all for now. In the next article I'll be writing about using this method to write special effects in Delphi. See you there!


Server Response from: ETNASC04