Skip to content

Converting to grayscale with TBitmap.ScanLine property

In my previous post "Boian’s TBitmap Visualizer and converting to grayscale" I have described an algorithm for converting arbitrary TBitmap instances to gray using a selected formula. The problem is with the performance of the code that actually changes the color of each pixel. Accessing color information is done via TBitmap.Pixels property, which is a two-dimensional array of TColor values. Since Delphi 3 there is a more efficient way of manipulating color information - using TBitmap.Scanline property. It is of pointer type and provides direct access to memory where color information is stored. The drawback is the fact that you have to be aware of TBitmap instance PixelFormat property to understand the layout in memory of individual bytes that make up the complete color information.

It is not a new page, but "Manipulating Pixels With Delphi’s ScanLine Property" tech note from efg’s Computer Lab is probably the ultimate reference to the topic, full of Danny Thorpe’s comments explaining the inner workings of TBitmap class implementation.

Accessing ScanLine property can be expensive in terms of performance, so it is the best to calculate the offset in bytes for every row in a bitmap just once and use it for performing pointer arithmetics as demonstrated by ScanlineTiming test application that opens and compiles beautifully in Delphi 2010 on Windows 7.

What interests me more is to compare performance of changing bitmaps using "Pixels" and "ScanLine" properties. Delphi 2010 introduced into the VCL the new TStopwatch type in "Diagnostics" unit that should be the most convenient way of measuring time.

Here is my rewritten ToGray procedure that is now taking one additional parameter "ba: TBitmapAccess" that controls how bitmaps are accessed: via ScanLine or via Pixels properties.


type
  TColor2Grayscale = (
    c2gAverage,
    c2gLightness,
    c2gLuminosity
  );

  TBitmapAccess = (
    baScanLine,
    baPixels
  );

// ...

function RGBToGray(R, G, B: byte; cg: TColor2Grayscale = c2gLuminosity): byte;
begin
  case cg of
    c2gAverage:
      Result := (R + G + B) div 3;

    c2gLightness:
      Result := (max(R, G, B) + min(R, G, B)) div 2;

    c2gLuminosity:
      Result := round(0.2989*R + 0.5870*G + 0.1141*B); // coeffs from Matlab

    else
      raise Exception.Create('Unknown Color2Grayscale value');
  end;
end;

// ...

procedure ToGray(aBitmap: Graphics.TBitmap;
  cg: TColor2Grayscale = c2gLuminosity;
  ba: TBitmapAccess = baScanLine);
var w, h: integer; CurrRow, OffSet: integer;
  x: byte; pRed, pGreen, pBlue: PByte;
begin
  if ba = baPixels then
  begin
    if aBitmap <> nil then
      for h := 0 to aBitmap.Height - 1 do
        for w := 0 to aBitmap.Width - 1 do
          aBitmap.Canvas.Pixels[w,h] :=
            ColorToGray(aBitmap.Canvas.Pixels[w,h], cg);
  end

  else // ba = baScanLine
  begin
    if aBitmap.PixelFormat <> pf24bit then
      raise Exception.Create(
        'Not implemented. PixelFormat has to be "pf24bit"');

    CurrRow := Integer(aBitmap.ScanLine[0]);
    OffSet := Integer(aBitmap.ScanLine[1]) - CurrRow;

    for h := 0 to aBitmap.Height - 1 do
    begin
      for w := 0 to aBitmap.Width - 1 do
      begin
        pBlue  := pByte(CurrRow + w*3);
        pGreen := pByte(CurrRow + w*3 + 1);
        pRed   := pByte(CurrRow + w*3 + 2);
        x := RGBToGray(pRed^, pGreen^, pBlue^, cg);
        pBlue^  := x;
        pGreen^ := x;
        pRed^   := x;
      end;
      inc(CurrRow, OffSet);
    end;
  end
end;

On average my test application shows that using "ScanLine" property for bitmap access is approximately 30 times faster then using "Pixels". That’s really more then I was expecting…

In the code above I have only implemented support for "pf24bit" pixel format.

In order to optimize code, I’m precalculating offset between scanlines to avoid too many calls to "ScanLine" as it may result in degraded performance. The other benefit of this approach is taking automatically into account byte padding in memory.

Using Pixels property (1556ms)

Using Pixels property (1556ms)

Using ScanLine property (17ms)

Using ScanLine property (17ms)

The source code for this test application can be downloaded from the Embarcadero Developer Network.

{ 7 } Comments

  1. Ken Knopfli | April 15, 2010 at 12:40 pm | Permalink

    Tried to download the source code, but I see no place to download.

    I am a registered user of D2010 and can download all updates.

  2. Ken Knopfli | April 15, 2010 at 12:43 pm | Permalink

    OK, sorry, now it works.

    Strange, I logged on at Embi, but still no button appeared.

    Then I came back here, clicked on the link a second time, and only then did the Download button appear.

  3. Mike | April 15, 2010 at 2:31 pm | Permalink

    You can even enhance the performance by a factor of 1.8 if you INLINE the RGBToGray function.

    What’s interesting though is that if you change the loop to:

    for h := 0 to aBitmap.Height - 1 do
    begin
    actW := 0;
    for w := 0 to aBitmap.Width - 1 do
    begin
    pBlue := pByte(CurrRow + actW);
    pGreen := pByte(CurrRow + actW + 1);
    pRed := pByte(CurrRow + actW + 2);
    x := RGBToGray(pRed^, pGreen^, pBlue^, cg);
    pBlue^ := x;
    pGreen^ := x;
    pRed^ := x;
    inc(actW, 3);
    end;
    inc(CurrRow, OffSet);
    end;
    end;

    the whole routine is getting slower although we removed the unnecessary (and normally slower than addition) multiplication there.

  4. Pawel Glowacki | April 15, 2010 at 3:54 pm | Permalink

    @Mike, I should have thought about inlining. You are right adding "inline" directive improves the code almost x2.

  5. Anders Isaksson | April 16, 2010 at 12:10 pm | Permalink

    The Scanline part will crash on a bitmap that is empty, or only contains one row of pixels. You need to make sure that aBitmap.Height is greater than one before accessing Scanline[0] and Scanline[1].

    Another advantage (apart from taking care of padding bytess) of using the Offset between Scanline[0] and Scanline[1] is that you don’t have to care for ‘top-down’ and ‘bottom-up’ bitmaps.

  6. Pawel Glowacki | April 16, 2010 at 1:29 pm | Permalink

    @Anders, good point.

    Added check for "aBitmap nil" and just modified this very line to:

    if aBitmap.Height > 1 then
    OffSet := Integer(aBitmap.ScanLine[1]) - CurrRow;

  7. handbags | June 14, 2012 at 10:52 am | Permalink

    This is the correct blog for anyone who needs to seek out out about this topic. You understand so much its virtually exhausting to argue with you (not that I truly would want匟aHa). You undoubtedly put a new spin on a topic thats been written about for years. Great stuff, just great! http://www..cnreplicalouisvuitton.com

Post a Comment

Your email is never published nor shared. Required fields are marked *

Bad Behavior has blocked 2 access attempts in the last 7 days.

Close