Fast direct pixel access
Introduction
Standard graphical LCL components provides Canvas object for common drawing. But most of available graphic routines have some overhead given by universality, platform independence and safety. To achieve best drawing speed it is necessary to use specialized bitmap structures and routines. There are some graphical libraries available for faster graphic processing with wide drawing functions. But for some specific usage it is necessary to create custom small library with data structures fitted for own application.
Direct pixel access in libraries is generally slowed-down by more factors:
- Coordinate limits checking
- Facility for automatic image updating and redrawing
- Abstract program constructions as property, static and virtual methods, dynamic two dimensional arrays
- Support for multiple pixel formats
- Support for multiple platforms
Pixel format
We can use simple integer pixels.
<delphi>TFastBitmapPixel = Integer;</delphi>
Or more abstract pixels with separated components
<delphi>TFastBitmapPixelComponents = packed record
Blue: Byte; Green: Byte; Red: Byte; Alpha: Byte;
end;</delphi>
Bitmap structure
Bitmap class should provide direct pixel access given by X, Y coordinate. But some graphic operation could be further optimized by not doing coordinate calculations for every pixel and rather do pixel pointer shifting by simple memory pointer addition. Some mass operation as filling rectangular region could be optimized using Move and FillChar functions.
Two dimensional dynamic array
This is native way to express two dimensional array in pascal. Internal structure is implemented as pointer to array of pointers to data because dynamic array is in fact pointer to array data. Then calculation of pixel position is matter of fetching pointer for rows and add horizontal position to it.
<delphi>interface
type
TFastBitmap = class private function GetSize: TPoint; procedure SetSize(const AValue: TPoint); public Pixels: array of array of TFastBitmapPixel; property Size: TPoint read GetSize write SetSize; end;
implementation
{ TFastBitmap }
function TFastBitmap.GetSize: TPoint; begin
Result.X := Length(Pixels); if Result.X > 0 then Result.Y := Length(Pixels[0]) else Result.Y := 0;
end;
procedure TFastBitmap.SetSize(const AValue: TPoint); begin
SetLength(Pixels, AValue.X, AValue.Y);
end;</delphi>
Raw dynamic memory
It is good to have whole bitmap in one compact memory area. Such memory block behave as video memory of video card. Position of pixels have to be calculated by using equation Y * Width + X with use of instructions for addition and multiplication. Access to pixels is pretty fast thanks to GetPixel and SetPixel methods inlining. But more instruction have to be used than in case of two dimensional dynamic array.
<delphi>interface
type
PFastBitmapPixel = ^TFastBitmapPixel; TFastBitmap = class private FPixelsData: PByte; FSize: TPoint; function GetPixel(X, Y: Integer): TFastBitmapPixel; inline; procedure SetPixel(X, Y: Integer; const AValue: TFastBitmapPixel); inline; procedure SetSize(const AValue: TPoint); public constructor Create; destructor Destroy; override; property Size: TPoint read FSize write SetSize; property Pixels[X, Y: Integer]: TFastBitmapPixel read GetPixel write SetPixel; end;
implementation
{ TFastBitmap }
function TFastBitmap.GetPixel(X, Y: Integer): TFastBitmapPixel; begin
Result := PFastBitmapPixel(FPixelsData + (Y * FSize.X + X) * SizeOf(TFastBitmapPixel))^;
end;
procedure TFastBitmap.SetPixel(X, Y: Integer; const AValue: TFastBitmapPixel); begin
PFastBitmapPixel(FPixelsData + (Y * FSize.X + X) * SizeOf(TFastBitmapPixel))^ := AValue;
end;
procedure TFastBitmap.SetSize(const AValue: TPoint); begin
if (FSize.X = AValue.X) and (FSize.Y = AValue.X) then Exit; FSize := AValue; FPixelsData := ReAllocMem(FPixelsData, FSize.X * FSize.Y * SizeOf(TFastBitmapPixel));
end;
constructor TFastBitmap.Create; begin
Size := Point(0, 0);
end;
destructor TFastBitmap.Destroy; begin
FreeMem(FPixelsData); inherited Destroy;
end;</delphi>
Drawing bitmap on screen
In this test let assume that we have simple bitmap structure designed as two dimensional byte array where each pixel have 256 possible colors. This could be gray image or some palette mapped image. All image manipulation will be done with custom functions with direct pixel access. Thanks to custom data structure functions could be optimized for faster block memory operations if necessary.
To be able to display image on Form custom bitmap have to be copied to some TWinControl canvas area. Image have to be copied repeatedly if motion image is generated. Every bitmap copy in memory take some time. Then our aim is to do as low as possible copy operations and rather copy our bitmap to screen directly if possible.
You can draw image as fast as possible in simple loop: <delphi>repeat
FastBitmapToBitmap(FastBitmap, Image1.Picture.Bitmap); Application.ProcessMessages;
until Terminated;</delphi>
Or draw image for example using Timer with defined drawing interval. Even if nothing is changed on bitmap there is no need to copy bitmap to screen so RedrawPending simple flag could be used. Thanks to delayed draw execution with calling Redraw method drawing of frames could be skipped.
<delphi>TForm1 = class(TForm) published
procedure Timer1Execute(Sender: TObject); ...
public
RedrawPending: Boolean; Drawing: Boolean; FastBitmap: TFastBitmap; procedure Redraw; ...
end;
procedure TForm1.Redraw; begin
RedrawPending := True;
end;
procedure TForm1.Timer1Execute(Sender: TObject); begin
if (not Drawing) and RedrawPending then try Drawing := True; CustomProcessing(FastBitmap); FastBitmapToBitmap(FastBitmap, Image1.Picture.Bitmap); finally RedrawPending := False; Drawing := False; end;
end;</delphi>
Methods
TBitmap.Canvas.Pixels
This is most straighforward but slowest method.
<delphi>function FastBitmapToBitmap(FastBitmap: TFastBitmap; Bitmap: TBitmap); var
X, Y: Integer;
begin
for X := 0 to FastBitmap.Size.X - 1 do for Y := 0 to FastBitmap.Size.Y - 1 do Bitmap.Canvas.Pixels[X, Y] := FastBitmap.Pixels[X, Y] * $010101;
end;</delphi>
TBitmap.Canvas.Pixels with Update locking
Previous method could be speeded up by update locking and thus redusing per pixel update and event signaling.
<delphi>function FastBitmapToBitmap(FastBitmap: TFastBitmap; Bitmap: TBitmap); var
X, Y: Integer;
begin
try Bitmap.BeginUpdate(True); for X := 0 to FastBitmap.Size.X - 1 do for Y := 0 to FastBitmap.Size.Y - 1 do Bitmap.Canvas.Pixels[X, Y] := FastBitmap.Pixels[X, Y] * $010101; finally Bitmap.EndUpdate(False); end;
end;</delphi>
TLazIntfImage
TBitmap is general bitmap component compatible with Delphi and it use TColor type for pixels. But LCL provide another component better suited for image handling. This component provide faster access to pixels and in addition it supports alpha channel.
<delphi>uses
..., LCLType, LCLProc, LCLIntf;
function FastBitmapToBitmap(FastBitmap: TFastBitmap; Bitmap: TBitmap); var
X, Y: Integer; TempIntfImage: TLazIntfImage;
begin
try TempIntfImage := Bitmap.CreateIntfImage; // Temp image could be precreated and holded owning class for X := 0 to FastBitmap.Size.X - 1 do for Y := 0 to FastBitmap.Size.Y - 1 do begin TempIntfImage.Colors[X, Y] := TColorToFPColor(FastBitmap.Pixels[X, Y] * $010101); end; Bitmap.LoadFromIntfImage(TempIntfImage); finally TempIntfImage.Free; end;
end;</delphi>
TBGRABitmap.ScanLine
Using TBitmap.ScanLine was method used frequently on Delphi. But TBitmap.ScanLine is not supported by LCL. ScanLine property give access to memory starting point for each row raw data. Then direct manipulation with pixels is much faster than using Pixels property as no additional events is fired. But pixel data is dependent on pixel format, row byte alignment and other parameters.
There is graphic library BGRABitmap which allow access to scan lines. Overall speed of this method is pretty good. Drawing is done directly to Canvas of some TWinControl components like TForm of TPaintBox.
<delphi>uses
..., BGRABitmap, BGRABitmapTypes;
procedure FastBitmapToCanvas(FastBitmap: TFastBitmap; Canvas: TCanvas); var
X, Y: Integer; P: PBGRAPixel;
begin
with FastBitmap do for Y := 0 to Size.Y - 1 do begin P := BGRABitmap.ScanLine[Y]; for X := 0 to Size.X - 1 do begin P^.Red := Pixels[X, Y]; P^.Green := Pixels[X, Y]; P^.Blue := Pixels[X, Y]; P^.Alpha := 255; Inc(P); end; end; BGRABitmap.InvalidateBitmap; // Changed by direct access BGRABitmap.Draw(Canvas, 0, 0, False);
end;</delphi>
BGRABitmap tutorial shows how to access directly to pixels
TBitmap.RawImage
This method is so far fastest in comparing to previous ones but more complicated as special care have to be given to bitmap data structure. Example assume that bitmap PixelFormat is pf24bit. Accessed raw data may differs across platforms.
<delphi>uses
..., GraphType;
function FastBitmapToBitmap(FastBitmap: TFastBitmap; Bitmap: TBitmap); var
X, Y: Integer; PixelPtr: PInteger; PixelRowPtr: PInteger; P: TPixelFormat; RawImage: TRawImage; BytePerPixel: Integer;
begin
try Bitmap.BeginUpdate(False); RawImage := Bitmap.RawImage; PixelRowPtr := PInteger(RawImage.Data); BytePerPixel := RawImage.Description.BitsPerPixel div 8; for Y := 0 to Size.Y - 1 do begin PixelPtr := PixelRowPtr; for X := 0 to Size.X - 1 do begin PixelPtr^ := Pixels[X, Y] * $010101; Inc(PByte(PixelPtr), BytePerPixel); end; Inc(PByte(PixelRowPtr), RawImage.Description.BytesPerLine); end; finally Bitmap.EndUpdate(False); end;
end;</delphi>
Speed comparison
This table shows only raw benchmark results which are dependent on used computer.
Method | Frame duration [ms] |
---|---|
TBitmap.Canvas.Pixels | 650 |
TBitmap.Canvas.Pixels with BeginUpdate and EndUpdate | 150 |
TLazIntfImage | 9 |
TBGRABitmap.ScanLine | 1.9 |
TBitmap.RawImage | 0.75 |