Delphi TBitmap Scanline
使用Dephi进行图像处理可以有多种方法,最常用的应该算是TBitmap,它提供方便的图像存取能力,结合Canvas可进行画线、画圆、图像拷贝等操作。
不过在进行大量的图像处理操作时,为了获得更高的速度,我们希望能够直接对图像缓冲区进行读写。查阅Dephi的帮助手册没有发现直接取得整个图像缓冲区的功能,
但提供的ScanLine属性可以取得指定行图像数据的指针,比较接近我们的要求,先看看ScanLine的描述:
property ScanLine[Row: Integer]: Pointer; Provides indexed access to each line of pixels.
ScanLine is used only with DIBs (Device Independent Bitmaps) for image editing tools that do low-level pixel work.
tagBITMAPINFOHEADER = record biSize: DWORD; biWidth: Longint; biHeight: Longint; biPlanes: Word; biBitCount: Word; biCompression: DWORD; biSizeImage: DWORD; biXPelsPerMeter: Longint; biYPelsPerMeter: Longint; biClrUsed: DWORD; biClrImportant: DWORD; end;
DWORD biSize;//指定结构需要的比特数。这个值不包含在结构的结尾被添上的颜色表或者颜色盒的大小。
LONG biWidth;//指定位图的像素宽度
LONG biHeight;//指定位图的高度。单位是像素。
biPlanes //Specifies the number of planes for the target device. This value must be set to 1.
biBitCount //指定每像素的比特数(bpp)。0 - JPEG or PNG, 1 - Monochrome
biCompression //对于压缩的视频和YUV格式,该数字是一个FOURCC代码。对于没有压缩的RGB,这个值是BI_RGB
DWORD biSizeImage;//位图的大小,单位是比特。对于没有压缩的RGB位图,该数值可以被设置为0
LONG biXPelsPerMeter;//规定了该位图的目标设备的水平模式,单位是像素每米
LONG biYPelsPerMeter;//规定了该位图的目标设备的垂直模式,单位是像素每米。
DWORD biClrUsed;//规定了颜色表中的颜色点数,它们被位图使用
DWORD biClrImportant;//规定了在位图中比较重要的颜色点数,如果值为0,那么所有的颜色都是重要的。
biWidth
The width of the bitmap, in pixels.
If biCompression is BI_JPEG or BI_PNG, the biWidth member specifies the width of the decompressed JPEG or PNG image file, respectively.
biHeight
The height of the bitmap, in pixels.
If biHeight is positive, the bitmap is a bottom-up DIB and its origin is the lower-left corner.
If biHeight is negative, the bitmap is a top-down DIB and its origin is the upper-left corner.
If biHeight is negative, indicating a top-down DIB, biCompression must be either BI_RGB or BI_BITFIELDS. Top-down DIBs cannot be compressed.
If biCompression is BI_JPEG or BI_PNG, the biHeight member specifies the height of the decompressed JPEG or PNG image file, respectively.
BI_RGB
An uncompressed format.
BI_BITFIELDS
Specifies that the bitmap is not compressed and that the color table consists of three DWORD color masks
that specify the red, green, and blue components, respectively, of each pixel.
This is valid when used with 16- and 32-bpp bitmaps.
function BytesPerScanline(PixelsPerScanline, BitsPerPixel, Alignment: Longint): Longint; begin Dec(Alignment); Result := ((PixelsPerScanline * BitsPerPixel) + Alignment) and not Alignment; Result := Result div 8; end;
function TBitmap.GetScanLine(Row: Integer): Pointer; begin Changing(Self); with FImage.FDIB, dsbm, dsbmih do begin if (Row < 0) or (Row >= bmHeight) then InvalidOperation(@SScanLine); DIBNeeded; GDIFlush; if biHeight > 0 then // bottom-up DIB Row := biHeight - Row - 1
else
Row := Row; // top-down DIB
Result := PByte(bmBits) + Row * BytesPerScanline(biWidth, biBitCount, 32); end; end;
procedure TForm1.Button1Click( Sender : TObject ); var BitMap : TBitmap; S : String; begin BitMap := TBitmap.Create; try BitMap.PixelFormat := pf24bit; // 24位色,每像素点3个字节 -- 首先设置 PixelFormat 否则出错 ! BitMap.Width := 1000; BitMap.Height := 2; // BitMap.Height := -2; -- Top-Down DIB [ ScanLine[0] -- First Row ] FmtStr( S, 'ScanLine[0]:%8x'#13'ScanLine[1]:%8x'#13'ScanLine[1]-ScanLine[0]:%d',
[ Integer( BitMap.ScanLine[ 0 ] ), Integer( BitMap.ScanLine[ 1 ] ), Integer( BitMap.ScanLine[ 1 ] ) - Integer( BitMap.ScanLine[ 0 ] ) ] );
MessageBox( Handle, PChar( S ), 'ScanLine', MB_OK ); finally if Assigned( BitMap ) then FreeAndNil( BitMap ); end; end
ScanLine[0]: E90BB8 ScanLine[1]: E90000 ScanLine[1]-ScanLine[0]:-3000
让我们再看看ScanLine[0]、ScanLine[1]的关系, ScanLine[0]与ScanLine[1]之间相差3000=1000像素宽×3字节这很容易理解,但为什么是负数呢?
因为BMP图像数据是“按行存放,每行按双字对齐,行按倒序方式存放”的,也就是说屏幕显示的第一行存放在最后,
屏幕显示的最后一行存放在前面,所以用ACDSee等看图软件查看尺寸较大的位图时先从下部开始显示就是这个道理。
从上面的结果可以看出TBitmap的图像数据在内存中是按行倒序连续存放的,
通过TBitmap.ScanLine[TBitmap.Height-1]可以取得首地址即图像缓冲区地址
Memory : Bottom-Up DIB : origin is the lower-left corner ScanLine[H-1] \ / ***************** ScanLine[H-2] \ / ***************** \/ ScanLine[1] /\ ***************** ScanLine[0] / \ ***************** Memory : Top-Down DIB : origin is the upper-left corner. ScanLine[0] ----- ***************** ScanLine[1] ----- ***************** ScanLine[H-2] ----- ***************** ScanLine[H-1] ----- *****************
Top-Down vs. Bottom-Up DIBs
http://msdn.microsoft.com/en-us/library/windows/desktop/dd407212(v=vs.85).aspx
If you are new to graphics programming, you might expect that a bitmap would be arranged in memory
so that the top row of the image appeared at the start of the buffer, followed by the next row, and so forth.
However, this is not necessarily the case. In Windows, device-independent bitmaps (DIBs) can be placed in memory
in two different orientations, bottom-up and top-down.
In a bottom-up DIB, the image buffer starts with the bottom row of pixels, followed by the next row up, and so forth.
The top row of the image is the last row in the buffer.
Therefore, the first byte in memory is the bottom-left pixel of the image.
In GDI, all DIBs are bottom-up. The following diagram shows the physical layout of a bottom-up DIB.
In a top-down DIB, the order of the rows is reversed.
The top row of the image is the first row in memory, followed by the next row down.
The bottom row of the image is the last row in the buffer.
With a top-down DIB, the first byte in memory is the top-left pixel of the image.
DirectDraw uses top-down DIBs. The following diagram shows the physical layout of a top-down DIB:
For RGB DIBs, the image orientation is indicated by the biHeight member of the BITMAPINFOHEADER structure.
If biHeight is positive, the image is bottom-up.
If biHeight is negative, the image is top-down.
DIBs in YUV formats are always top-down, and the sign of the biHeight member is ignored.
Decoders should offer YUV formats with positive biHeight, but they should also accept YUV formats
with negative biHeight and ignore the sign.
Also, any DIB type that uses a FOURCC in the biCompression member, should express its biHeight as a positive number no matter
what its orientation is, since theFOURCC itself identifies a compression scheme whose image orientation should be
understood by any compatible filter.
function CreateDIB_TopDown( nWidth, nHeight : Integer ) : HBITMAP; var hbm : HBITMAP; hdcMem : HDC; bmi : BITMAPINFO;
pvBits : LPBYTE; // ScanLine[0] begin hdcMem := CreateCompatibleDC( 0 ); if hdcMem > 0 then begin ZeroMemory( @( bmi.bmiHeader ), sizeof( BITMAPINFOHEADER ) ); bmi.bmiHeader.biSize := sizeof( BITMAPINFOHEADER ); bmi.bmiHeader.biWidth := nWidth; bmi.bmiHeader.biHeight := -nHeight; // Use a top-down DIB bmi.bmiHeader.biPlanes := 1; bmi.bmiHeader.biBitCount := 32; hbm := CreateDIBSection( hdcMem, bmi, DIB_RGB_COLORS, Pointer( pvBits), 0, 0 ); if ( hbm > 0 ) then begin// Fill in the pixels of the bitmap end; DeleteDC( hdcMem ); end; Result := hbm; end;
Bitmap data area
This is a byte stream that describes the pixels of the image (this may or may not be in compressed form) in 1-, 4-, 8-, 16-, or 24-bit format.
The data is in line-by-line order, but it may be upside-down so that the first line of data is the last line of the image.
You can detect this by looking at the sign of biHeight — a positive sign means the bitmap is upside-down,
and a negative sign means the bitmap is normal.
最后补充说明一下:
- Bitmap图像缓冲区是双节对齐的,如果把例1中的图像宽度改为999,一个像素行还是占3000个字节。
- 目前Bitmap.PixelFormat有pfDevice、pf1bit、pf4bit、pf8bit、pf15bit、pf16bit、pf24bit、pf32bit、pfCustom共9种,
不同格式每个像素所占字节数不同,其中pf4bit和pf8bit格式的图像缓冲区保存的为颜色索引号,真正的颜色值在调色板中,
pf15bit、pf16bit格式中RGB所占的位数(Bit)不一定是等长的。
通过直接对图像缓冲区的读写将图像淡出到黑色
procedure TForm1.Button1Click( Sender : TObject ); const FADEOUT_STEP = 24; // 淡出衰减值 FIX_WIDTH = 320; FIX_HEIGHT = 200; var BitMap : TBitmap; hWinDC : HDC; flagAgein : Boolean; lpBuffer : PByte; // 图像缓冲区指针 begin BitMap := TBitmap.Create; if not Assigned( BitMap ) then Exit; try // 设置位图格式、宽度、高度 BitMap.PixelFormat := pf24bit; BitMap.Width := FIX_WIDTH; BitMap.Height := FIX_HEIGHT; // 设置Form的宽充、高度,便于显示结果 Button1.Visible := false; ClientWidth := FIX_WIDTH; ClientHeight := FIX_HEIGHT; // 拷贝图像到Bitmap中 hWinDC := GetDC( 0 ); if ( hWinDC <> NULL ) then BitBlt( BitMap.Canvas.Handle, 0, 0, FIX_WIDTH, FIX_HEIGHT, hWinDC, 0, 0, SRCCOPY ) else BitBlt( BitMap.Canvas.Handle, 0, 0, FIX_WIDTH, FIX_HEIGHT, Canvas.Handle, 0, 0, SRCCOPY ); repeat flagAgein := false; lpBuffer := BitMap.ScanLine[ FIX_HEIGHT - 1 ];
// 取得图像缓冲区首地址 Integer(BitMap.ScanLine[0]) + FIX_WIDTH*3 为图像缓冲区结束地址 while Integer( lpBuffer ) < Integer( BitMap.ScanLine[ 0 ] ) + FIX_WIDTH * 3 do begin if lpBuffer^ > FADEOUT_STEP then begin Dec( lpBuffer^, FADEOUT_STEP ); flagAgein := true; end else lpBuffer^ := 0; Inc( lpBuffer ); Application.ProcessMessages; end; Canvas.Draw( 0, 0, BitMap ); until ( not flagAgein ); MessageBox( Handle, 'Done', 'Fadeout', MB_OK ); finally if Assigned( BitMap ) then FreeAndNil( BitMap ); Button1.Visible := true; end; end;
Bitmap.Scanline for PixelFormat=pf1bit, pf8bit, pf24bit
Since someone from Italy asked me for an example of using pf1bit Bitmaps, I thought I would post part of my response and add other details for pf8bit and pf24bit here in case others were wondering.
Background
The new Delphi 3 scanline property allows quick access to individual pixels, but you must know what Bitmap.PixelFormat you're working with before you can access the pixels.
Possible PixelFormats include:
pfDevice pf1bit pf4bit pf8bit pf15bit pf16bit pf24bit pf32bit
For pf24bit bitmaps, I define (I wish Borland would)
CONST PixelCountMax = 32768; TYPE pRGBArray = ^TRGBArray; TRGBArray = ARRAY [ 0 .. PixelCountMax - 1 ] OF TRGBTriple;
Note: TRGBTriple is defined in the Windows.PAS unit.
To step through a 24-bit bitmap and while creating a new one and access the 3-bytes-per-pixel data, use a construct like the following
VAR i : INTEGER; j : INTEGER; RowOriginal : pRGBArray; RowProcessed : pRGBArray; BEGIN IF OriginalBitmap.PixelFormat <> pf24bit THEN RAISE EImageProcessingError.Create( 'GetImageSpace: ' + 'Bitmap must be 24-bit color.' ); { Step through each row of image. } FOR j := OriginalBitmap.Height - 1 DOWNTO 0 DO BEGIN RowOriginal := pRGBArray( OriginalBitmap.Scanline[ j ] ); RowProcessed := pRGBArray( ProcessedBitmap.Scanline[ j ] ); FOR i := OriginalBitmap.Width - 1 DOWNTO 0 DO BEGIN // Access individual color RGB color planes with references like: // RowProcessed[i].rgbtRed := RowOriginal[i].rgbtRed; // RowProcessed[i].rgbtGreen := RowOriginal[i].rgbtGreen; // RowProcessed[i].rgbtBlue := RowOriginal[i].rgbtBlue; END END END;
pf8bit Bitmaps
Access to these byte-per-pixel bitmaps is easy using the TByteArray (defined in SysUtils.PAS)
PByteArray = ^TByteArray; TByteArray = array[0..32767] of Byte;
I suppose, but I've never tried it, you could access pf16bit Bitmaps using the following defined in SysUtils.PAS:
PWordArray = ^TWordArray; TWordArray = array[0..16383] of Word; )
To process an 8-bit (pf8bit) bitmap, use a construct like the following that constructs a histogram of such a bitmap:
VAR Histogram : THistogram; i : INTEGER; j : INTEGER; Row : pByteArray; FOR i := Low( THistogram ) TO High( THistogram ) DO
Histogram[ i ] := 0;
IF Bitmap.PixelFormat = pf8bit THEN BEGIN FOR j := Bitmap.Height - 1 DOWNTO 0 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := Bitmap.Width - 1 DOWNTO 0 DO BEGIN INC( Histogram[ Row[ i ] ] ) END END END
pf1bit Bitmaps
Accessing pf8bit bitmaps is easy since they are one byte per pixel.
But you can save a lot of memory if you only need a single bit per pixel (such as with various masks), if you use pf1bit Bitmaps.
As with pf8bit bitmaps, use a TByteArray to access pf1bit Scanlines.
But you will need to perform bit operations on the bytes to access the various pixels.
Also, the width of the Scanline is Bitmap.Width DIV 8 bytes.
The following code shows how to create the following kinds of 1-bit bitmaps:
black, white, stripes, "g", "arrow" and random -- an "invert" option is also available.
(Send me an E-mail if you'd like the complete working source code including the form.)
Create a form with an Image1: TImage on it -- I used 1 256x256 Image1 with Stretch := TRUE to see the individual pixels more easily.
The buttons Black, White and Stripes have tags of 0, 255, and 85 ($55 = 01010101 binary) that call ButtonStripesClick when selected.
Buttons "g" and "arrow" call separate event handlers to draw these bitmaps taken form HP Laserjet examples.
"Random" just randomly sets bits on in the 1-bit bitmaps.
"Invert" changes all the 0s to 1's and vice versa.
// Example of how to use Bitmap.Scanline for PixelFormat=pf1Bit. // Requested by Mino Ballone from Italy. // // Copyright (C) 1997, Earl F. Glynn, Overland Park, KS. All rights reserved. // May be freely used for non-commerical purposes. unit ScreenSingleBit; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TForm1 = class( TForm ) Image1 : TImage; ButtonBlack : TButton; ButtonWhite : TButton; ButtonStripes : TButton; ButtonG : TButton; ButtonArrow : TButton; ButtonRandom : TButton; ButtonInvert : TButton; procedure ButtonStripesClick( Sender : TObject ); procedure ButtonGClick( Sender : TObject ); procedure FormCreate( Sender : TObject ); procedure FormDestroy( Sender : TObject ); procedure ButtonRandomClick( Sender : TObject ); procedure ButtonInvertClick( Sender : TObject ); procedure ButtonArrowClick( Sender : TObject ); private Bitmap : TBitmap; { Private declarations } public { Public declarations } end; var Form1 : TForm1; implementation {$R *.DFM} CONST BitsPerPixel = 8; procedure TForm1.ButtonStripesClick( Sender : TObject ); VAR i : INTEGER; j : INTEGER; Row : pByteArray; Value : BYTE; begin Value := ( Sender AS TButton ).Tag; // Value = $00 = 00000000 binary for black // Value = $FF = 11111111 binary for white // Value = $55 = 01010101 binary for black & white stripes FOR j := 0 TO Bitmap.Height - 1 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := 0 TO ( Bitmap.Width DIV BitsPerPixel ) - 1 DO BEGIN Row[ i ] := Value END END; Image1.Picture.Graphic := Bitmap end; procedure TForm1.ButtonGClick( Sender : TObject ); CONST { The "g" bitmap was adapted from the LaserJet IIP Printer Tech Ref Manual } G : ARRAY [ 0 .. 31, 0 .. 3 ] OF BYTE = { 0 } ( ( $00, $FC, $0F, $C0 ), { 00000000 11111100 00001111 11000000 } { 1 } ( $07, $FF, $1F, $E0 ), { 00000111 11111111 00011111 11100000 } { 2 } ( $0F, $FF, $9F, $C0 ), { 00001111 11111111 10011111 11000000 } { 3 } ( $3F, $D7, $DE, $00 ), { 00111111 11010111 11011110 00000000 } { 4 } ( $3E, $01, $FE, $00 ), { 00111110 00000001 11111110 00000000 } { 5 } ( $7C, $00, $7E, $00 ), { 01111100 00000000 01111110 00000000 } { 6 } ( $78, $00, $7E, $00 ), { 01111000 00000000 01111110 00000000 } { 7 } ( $F0, $00, $3E, $00 ), { 11110000 00000000 00111110 00000000 } { 8 } ( $F0, $00, $3E, $00 ), { 11110000 00000000 00111110 00000000 } { 9 } ( $F0, $00, $1E, $00 ), { 11110000 00000000 00011110 00000000 } { 10 } ( $F0, $00, $1E, $00 ), { 11110000 00000000 00011110 00000000 } { 11 } ( $F0, $00, $1E, $00 ), { 11110000 00000000 00011110 00000000 } { 12 } ( $F0, $00, $1E, $00 ), { 11110000 00000000 00011110 00000000 } { 13 } ( $F0, $00, $3E, $00 ), { 11110000 00000000 00111110 00000000 } { 14 } ( $78, $00, $3E, $00 ), { 01111000 00000000 00111110 00000000 } { 15 } ( $78, $00, $3E, $00 ), { 01111000 00000000 00111110 00000000 } { 16 } ( $78, $00, $7E, $00 ), { 01111000 00000000 01111110 00000000 } { 17 } ( $3C, $00, $FE, $00 ), { 00111100 00000000 11111110 00000000 } { 18 } ( $1F, $D7, $DE, $00 ), { 00011111 11010111 11011110 00000000 } { 19 } ( $0F, $FF, $5E, $00 ), { 00001111 11111111 10011110 00000000 } { 20 } ( $07, $FF, $1E, $00 ), { 00000111 11111111 00011110 00000000 } { 21 } ( $00, $A8, $1E, $00 ), { 00000000 10101000 00011110 00000000 } { 22 } ( $00, $00, $1E, $00 ), { 00000000 00000000 00011110 00000000 } { 23 } ( $00, $00, $1E, $00 ), { 00000000 00000000 00011110 00000000 } { 24 } ( $00, $00, $1E, $00 ), { 00000000 00000000 00011110 00000000 } { 25 } ( $00, $00, $3E, $00 ), { 00000000 00000000 00111110 00000000 } { 26 } ( $00, $00, $3C, $00 ), { 00000000 00000000 00111100 00000000 } { 27 } ( $00, $00, $7C, $00 ), { 00000000 00000000 01111100 00000000 } { 28 } ( $00, $01, $F8, $00 ), { 00000000 00000001 11111000 00000000 } { 29 } ( $01, $FF, $F0, $00 ), { 00000001 11111111 11110000 00000000 } { 30 } ( $03, $FF, $E0, $00 ), { 00000011 11111111 11100000 00000000 } { 31 } ( $01, $FF, $80, $00 ) { 00000001 11111111 10000000 00000000 } ); VAR i : INTEGER; j : INTEGER; Row : pByteArray; begin FOR j := 0 TO Bitmap.Height - 1 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := 0 TO ( Bitmap.Width DIV BitsPerPixel ) - 1 DO BEGIN Row[ i ] := G[ j, i ] END END; Image1.Picture.Graphic := Bitmap end; procedure TForm1.ButtonArrowClick( Sender : TObject ); CONST { The "arrow" bitmap was adapted from the LaserJet IIP Printer Tech Ref Manual } Arrow : ARRAY [ 0 .. 31, 0 .. 3 ] OF BYTE = { 0 } ( ( $00, $00, $80, $00 ), { 00000000 00000000 10000000 00000000 } { 1 } ( $00, $00, $C0, $00 ), { 00000000 00000000 11000000 00000000 } { 2 } ( $00, $00, $E0, $00 ), { 00000000 00000000 11100000 00000000 } { 3 } ( $00, $00, $F0, $00 ), { 00000000 00000000 11110000 00000000 } { 4 } ( $00, $00, $F8, $00 ), { 00000000 00000000 11111000 00000000 } { 5 } ( $00, $00, $FC, $00 ), { 00000000 00000000 11111100 00000000 } { 6 } ( $00, $00, $FE, $00 ), { 00000000 00000000 11111110 00000000 } { 7 } ( $00, $00, $FF, $00 ), { 00000000 00000000 11111111 00000000 } { 8 } ( $00, $00, $FF, $80 ), { 00000000 00000000 11111111 10000000 } { 9 } ( $FF, $FF, $FF, $C0 ), { 11111111 11111111 11111111 11000000 } { 10} ( $FF, $FF, $FF, $E0 ), { 11111111 11111111 11111111 11100000 } { 11} ( $FF, $FF, $FF, $F0 ), { 11111111 11111111 11111111 11110000 } { 12} ( $FF, $FF, $FF, $F8 ), { 11111111 11111111 11111111 11111000 } { 13} ( $FF, $FF, $FF, $FC ), { 11111111 11111111 11111111 11111100 } { 14} ( $FF, $FF, $FF, $FE ), { 11111111 11111111 11111111 11111110 } { 15} ( $FF, $FF, $FF, $FF ), { 11111111 11111111 11111111 11111111 } { 16} ( $FF, $FF, $FF, $FF ), { 11111111 11111111 11111111 11111111 } { 17} ( $FF, $FF, $FF, $FE ), { 11111111 11111111 11111111 11111110 } { 18} ( $FF, $FF, $FF, $FC ), { 11111111 11111111 11111111 11111100 } { 19} ( $FF, $FF, $FF, $F8 ), { 11111111 11111111 11111111 11111000 } { 20} ( $FF, $FF, $FF, $F0 ), { 11111111 11111111 11111111 11110000 } { 21} ( $FF, $FF, $FF, $E0 ), { 11111111 11111111 11111111 11100000 } { 22} ( $FF, $FF, $FF, $C0 ), { 11111111 11111111 11111111 11000000 } { 23} ( $00, $00, $FF, $80 ), { 00000000 00000000 11111111 10000000 } { 24} ( $00, $00, $FF, $00 ), { 00000000 00000000 11111111 00000000 } { 25} ( $00, $00, $FE, $00 ), { 00000000 00000000 11111110 00000000 } { 26} ( $00, $00, $FC, $00 ), { 00000000 00000000 11111100 00000000 } { 27} ( $00, $00, $F8, $00 ), { 00000000 00000000 11111000 00000000 } { 28} ( $00, $00, $F0, $00 ), { 00000000 00000000 11110000 00000000 } { 29} ( $00, $00, $E0, $00 ), { 00000000 00000000 11100000 00000000 } { 30} ( $00, $00, $C0, $00 ), { 00000000 00000000 11000000 00000000 } { 31} ( $00, $00, $80, $00 ) { 00000000 00000000 10000000 00000000 } );
VAR i : INTEGER; j : INTEGER; Row : pByteArray; begin FOR j := 0 TO Bitmap.Height - 1 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := 0 TO ( Bitmap.Width DIV BitsPerPixel ) - 1 DO BEGIN Row[ i ] := Arrow[ j, i ] END END; Image1.Picture.Graphic := Bitmap end; procedure TForm1.FormCreate( Sender : TObject ); begin Bitmap := TBitmap.Create; WITH Bitmap DO BEGIN Width := 32; Height := 32; PixelFormat := pf1bit END; Image1.Picture.Graphic := Bitmap end; procedure TForm1.FormDestroy( Sender : TObject ); begin Bitmap.Free end; procedure TForm1.ButtonRandomClick( Sender : TObject ); VAR i : INTEGER; j : INTEGER; Row : pByteArray; begin FOR j := 0 TO Bitmap.Height - 1 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := 0 TO ( Bitmap.Width DIV BitsPerPixel ) - 1 DO BEGIN Row[ i ] := Random( 256 ) END END; Image1.Picture.Graphic := Bitmap end; procedure TForm1.ButtonInvertClick( Sender : TObject ); VAR i : INTEGER; j : INTEGER; Row : pByteArray; begin FOR j := 0 TO Bitmap.Height - 1 DO BEGIN Row := pByteArray( Bitmap.Scanline[ j ] ); FOR i := 0 TO ( Bitmap.Width DIV BitsPerPixel ) - 1 DO BEGIN Row[ i ] := NOT Row[ i ] END END; Image1.Picture.Graphic := Bitmap end; end.
http://stackoverflow.com/questions/13583451/how-to-use-scanline-property-for-24-bit-bitmaps
How to use ScanLine property for 24-bit bitmaps?
1. Introduction
In this post I'll try to explain the ScanLine
property usage only for 24-bit bitmap pixel format and if you actually need to use it.
As first take a look what makes this property so important.
2. ScanLine or not...?
You can ask yourself why to use such tricky technique like using ScanLine
property seemingly is
when you can simply use Pixels
to access your bitmap's pixels.
The answer is a big performance difference noticable when you perform pixel modifications even on a relatively small pixel area.
The Pixels
property internally uses Windows API functions - GetPixel
andSetPixel
, for getting and setting device context color values.
The performance lack at Pixels
technique is that you usually need to get pixel color values before you modify them,
what internally means the call of both mentioned Windows API functions.
The ScanLine
property is winning this race because provides a direct access to the memory where the bitmap pixel data are stored.
And direct memory access is just faster than two Windows API function calls.
But, it doesn't mean that Pixels
property is totally bad and that you should avoid to use it in all cases.
When you are going to modify just few pixels (not a big areas) occasionally e.g., then Pixels
might be sufficient for you.
But don't use it when you are going to manipulate with a pixel area.
3. Deep inside the pixels
3.1 Raw data
Pixel data of a bitmap (let's call them raw data for now) you can imagine as a single dimensional array of bytes,
containing the sequence of intensity values of color components for every single pixel.
Every pixel in bitmap consists from a fixed count of bytes depending on used pixel format.
For instance, the 24-bit pixel format has 1 byte for each of its color components - for red, green and blue channel.
The following picture illustrates how to imagine raw data byte array for such 24-bit bitmap.
Each colored rectangle here represents one byte:
3.2 Case study
Imagine you have a 24-bit bitmap 3x2 pixels (width 3px; height 2px) and keep it in your mind
because I'll try to explain some internals and show a principle of ScanLine
property usage on it.
It is so small just because of space needed for a deep view inside
(for those having a bright sight is a green example of such image in png format here
↘ ↙ :-)
3.3 Pixel composition
As first let's take a look how the pixel data of our bitmap image are internally stored; look at the raw data.
The following image shows the raw data byte array, where you can see each byte of our tiny bitmap with its index in that array.
You can also notice, how the groups of 3 bytes forms the individual pixels, and on which coordinates are these pixels situated on our bitmap:
[ picture ]
Another view of the same provides the following image. Each box represents one pixel of our imaginary bitmap there.
In each pixel you can see its coordinates and the group of 3 bytes with their indexes from the raw databyte array:
[ picture ]
4. Living with colors
4.1. Initial values
As we already know, pixels in our imaginary 24-bit bitmap are composed from 3 bytes - 1 byte for each color channel.
When you've created this bitmap in your imagination, all of those bytes in all pixels
have been against your will initialized to the max byte value - to 255. It means that all channels have now the maximal color intensities:
When we take a look, which color is mixed from these initial channel values for each pixel, we'll see that our bitmap is entirely white
.
So, when you create a 24-bit bitmap in Delphi, it is initially white.
Well, white will be bitmap in every pixel format by default, but they may differ in initial raw data byte values.
5. Secret life of ScanLine
From the above reading I hope you understood, how the bitmap data are stored in a raw data byte array and how the individual pixels are formed from these data.
Now move on to the ScanLine
property itself and how can be useful in a direct raw data processing.
5.1. ScanLine purpose
A main dish of this post, the ScanLine
property, is a read only indexed property that returns pointer
to the first byte of the array of raw databytes that belongs to a specified row in a bitmap.
In other words we request the access to the array of raw data bytes for a given row and what we receive is a pointer to the first byte of that array.
The index parameter of this property specifies the 0 based index of a row for which we want to get these data.
The following image illustrates our imaginary bitmap and the pointers we get by the ScanLine
property using different row indexes:
5.2. ScanLine advantage
So, from what we know, we can summarize that ScanLine
gives us a pointer to a certain row data byte array.
And with that row array of raw data we can work - we can read or overwrite its bytes,
but only in a range of the array bounds of a particular row:
Well, we have an array of color intensities for each pixel of a certain row. Considering iteration of such array;
it wouldn't be much comfortable to loop through this array by one byte and adjust only one of 3 color portions of a pixel.
Better will be loop through the pixels and adjust all 3 color bytes at once with each iteration - just like with Pixels
as we used to do.
5.3. Jumping through the pixels
To simplify a row array loop we need a structure matching our pixel data.
Fortunately, for 24-bit bitmaps there's the RGBTRIPLE
structure; in Delphi translated like TRGBTriple
.
This structure, in short looks like this (each member there represents intensity of one color channel):
type TRGBTriple = packed record rgbtBlue: Byte; rgbtGreen: Byte; rgbtRed: Byte; end;
Since I've tried to be tolerant to those having Delphi version below 2009 and because it makes the code somehow more understandable
I won't use pointer arithmetic for iteration, but a fixed length array with a pointer to it in the following examples
(pointer arithmetic would be less readable in Delphi 2009 below).
So, we have the TRGBTriple
structure for a pixel and now we define a type for the row array.
This will simplify the iteration of bitmap row pixels. This one I just borrowed from the ShadowWnd.pas unit
(home of one interesting class, anyway). Here it is:
type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple;
As you can see, it has a limit of 4096 pixels for a row, what should be enough for usually wide images.
If this won't be sufficient for you, just increase the high bound.
6. ScanLine in practice
6.1. Make the second row black
Let's start with the first example. In that we objectify our imaginary bitmap, set it proper width, height and pixel format (or if you want, a bit depth). Then we use ScanLine
with row parameter 1 to get pointer to the second row's raw data byte array. The pointer we get we'll assign to theRowPixels
variable which points to the array of TRGBTriple
, so since that time we can take it as an array of row pixels. Then we iterate this array in the whole width of the bitmap and set all color values of each pixel to 0, which results to a bitmap with the first row white (white is by default, as mentioned above) and what makes the second row black. This bitmap is then saved to file, but don't be surprised when you see it, it's really very small:
type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple; procedure TForm1.Button1Click(Sender: TObject); var I: Integer; Bitmap: TBitmap; Pixels: PRGBTripleArray; begin Bitmap := TBitmap.Create; try Bitmap.Width := 3; Bitmap.Height := 2; Bitmap.PixelFormat := pf24bit; // get pointer to the second row's raw data Pixels := Bitmap.ScanLine[1]; // iterate our row pixel data array in a whole width for I := 0 to Bitmap.Width - 1 do begin Pixels[I].rgbtBlue := 0; Pixels[I].rgbtGreen := 0; Pixels[I].rgbtRed := 0; end; Bitmap.SaveToFile('c:\Image.bmp'); finally Bitmap.Free; end; end;
6.2. Grayscale bitmap using luminance
As a sort of a meaningful example I'm posting here a procedure for grayscaling bitmaps using luminance. It uses the iteration of all bitmap rows from top to bottom. For each row is then obtained pointer to a raw dataand as before taken as the array of pixels. For each pixel of that array is then computed luminance value by this formula:
Luminance = 0.299 R + 0.587 G + 0.114 B
This luminance value is then assigned to each color component of the iterated pixel:
type PRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = array[0..4095] of TRGBTriple; procedure GrayscaleBitmap(ABitmap: TBitmap); var X: Integer; Y: Integer; Gray: Byte; Pixels: PRGBTripleArray; begin // iterate bitmap from top to bottom to get access to each row's raw data for Y := 0 to ABitmap.Height - 1 do begin // get pointer to the currently iterated row's raw data Pixels := ABitmap.ScanLine[Y]; // iterate the row's pixels from left to right in the whole bitmap width for X := 0 to ABitmap.Width - 1 do begin // calculate luminance for the current pixel by the mentioned formula Gray := Round((0.299 * Pixels[X].rgbtRed) + (0.587 * Pixels[X].rgbtGreen) + (0.114 * Pixels[X].rgbtBlue)); // and assign the luminance to each color component of the current pixel Pixels[X].rgbtRed := Gray; Pixels[X].rgbtGreen := Gray; Pixels[X].rgbtBlue := Gray; end; end; end;
And the possible usage of the above procedure. Notice, that you can use this procedure only for 24-bit bitmaps:
procedure TForm1.Button1Click(Sender: TObject); var Bitmap: TBitmap; begin Bitmap := TBitmap.Create; try Bitmap.LoadFromFile('c:\ColorImage.bmp'); if Bitmap.PixelFormat <> pf24bit then raise Exception.Create('Incorrect bit depth, bitmap must be 24-bit!'); GrayscaleBitmap(Bitmap); Bitmap.SaveToFile('c:\GrayscaleImage.bmp'); finally Bitmap.Free; end; end;
http://edn.embarcadero.com/article/29173
How to Use Scanlines
This article shows a fast way to manipulate bitmaps, without using canvas : the scanlines.
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.
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;
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!
http://www.efg2.com/Lab/ImageProcessing/Scanline.htm
Manipulating Pixels With Delphi's ScanLine Property
http://wiki.freepascal.org/Fast_direct_pixel_access
Pixel format
We can use simple integer pixels which would be faster on 32-bit platform:
TFastBitmapPixel = Integer;
Or more abstract pixels with separated components:
TFastBitmapPixelComponents = packed record Blue: Byte; Green: Byte; Red: Byte; Alpha: Byte; end;
It is possible even go further to bit level and define 16-bit RGB pixel used for some LCD displays:
TFastBitmapPixelComponents16Bit = packed record Blue: 0..31; // 5 bits Green: 0..63; // 6 bits Red: 0..31; // 5 bits end;
Pixel can be pointer which would be useful for cases where pixel value itself is rather large or can be compressed somehow.
TFastBitmapPixelComponentsValue = packed record Blue: Word; Green: Word; Red: Word; Alpha: Word; end; TFastBitmapPixelComponents = ^TFastBitmapPixelComponentsValue;
Another situation is use of polymorphism of classes.
TFastBitmapPixel = class procedure Clear; virtual; end; TFastBitmapPixelComponents = class(TFastBitmapPixel) Blue: Word; Green: Word; Red: Word; Alpha: Word; procedure Clear; override; end; TFastBitmapPixelByte = class(TFastBitmapPixel) Value: Byte; procedure Clear; override; end;
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.
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;
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.
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;
Strict Pointer pixel access
We are able eliminate some of coordinate multiplications with low level pixel access using pointers only. Then only addition(incrementation) is necessary to change current pixel position.
interface type TFastBitmap = class private FPixelsData: PByte; FSize: TPoint; procedure SetSize(const AValue: TPoint); public constructor Create; destructor Destroy; override; procedure RandomImage; property Size: TPoint read FSize write SetSize; function GetPixelAddress(X, Y: Integer): PFastBitmapPixel; inline; function GetPixelSize: Integer; inline; end; implementation { TFastBitmap } 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 FreeData(FPixelData); inherited Destroy; end; function TFastBitmap.GetPixelAddress(X, Y: Integer): PFastBitmapPixel; begin Result := PFastBitmapPixel(FPixelsData) + Y * FSize.X + X; end; function TFastBitmap.GetPixelSize: Integer; begin Result := SizeOf(TFastBitmapPixel); end;
In this case drawing pixels is less readable:
procedure RandomImage(FastBitmap: TFastBitmap); var X, Y: Integer; PRow: PFastBitmapPixel; PPixel: PFastBitmapPixel; begin with FastBitmap do begin PRow := GetPixelAddress(0, Size.Y div 2); for Y := 0 to Size.Y - 1 do begin PPixel := PRow; for X := 0 to Size.X - 1 do begin PPixel^ := Random(256) or (Random(256) shl 16) or (Random(256) shl 8); Inc(PPixel); end; Inc(PRow, Size.X); end; end; end;
Pixel operation optimization
Basic line algorithm
This is naive form which is readable but with price of slower processing.
procedure TFastBitmap.HorizontalLine(X, Y, Length: Integer; Color: TFastBitmapPixel); var I: Integer; begin for I := 0 to Length - 1 do Pixels[X + I, Y] := Color; end;
Pointers
With use of pointers we can eliminate much of pixel address addition and multiplication by Pixels property access. Only fast increment operation is performed.
procedure TFastBitmap.HorizontalLine(X, Y, Length: Integer; Color: TFastBitmapPixel); var I: Integer; P: PFastBitmapPixel; begin P := PFastBitmapPixel(FPixelData + (Y * Size.X + X) * SizeOf(TFastBitmapPixel)); for I := 0 to Length - 1 do begin P^ := Color; Inc(P); end; end;
Mass fill using FillDWord
Access using pointers and incrementation is fastest possible using conventional single operations. But most of todays CPU offer instructions for mass operations like MOVS, STOS for x86 architecture. Pixel size should be 1, 2 or 4 bytes to be able to use this optimization.
procedure TFastBitmap.HorizontalLine(X, Y, Length: Integer; Color: TFastBitmapPixel); var I: Integer; P: PFastBitmapPixel; begin P := PFastBitmapPixel(FPixelData + (Y * Size.X + X) * SizeOf(TFastBitmapPixel)); FillDWord(P^, Length, Color); end;
Inlining
If code is notably smaller like SetPixel and GetPixel methods it is better to inline instructions rather than do push and pop operations on stact with execution of call and ret instruction. This optimization will be even significant if such operation is executed many times as pixel operations do.
procedure TFastBitmap.HorizontalLine(X, Y, Length: Integer; Color: TFastBitmapPixel); inline; var I: Integer; P: PFastBitmapPixel; begin P := PFastBitmapPixel(FPixelData + (Y * Size.X + X) * SizeOf(TFastBitmapPixel)); FillDWord(P^, Length, Color); end;
DMA
If memory block have to be copied to another memory place or device memory DMA(Direct Memory Access) can be used. CPU doesn't have to be involved in copy operations and can do further processing. This kind of optimization can be used in OpenGL for copying data to video card memory.
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:
repeat FastBitmapToBitmap(FastBitmap, Image1.Picture.Bitmap); Application.ProcessMessages; until Terminated;
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.
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;
Draw methods
TBitmap.Canvas.Pixels
This is most straighforward but slowest method. Suppose we have grayscale values from 0 to 255 in our TFastBitmap, then to copy it on the TBitmap, we could write this :
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;
TBitmap.Canvas.Pixels with Update locking
Previous method could be speeded up by update locking and thus redusing per pixel update and event signaling.
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;
TBGRABitmap.ScanLine
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. The pixel format is 32-bit color with alpha channel, i.e. 8-bit for each channel.
Using TBitmap.ScanLine was a 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.
We can copy our grayscale FastBitmap data to a TBGRABitmap to render it on the screen.
uses ..., BGRABitmap, BGRABitmapTypes; procedure FastBitmapToCanvas(FastBitmap: TFastBitmap; Canvas: TCanvas); var X, Y: Integer; P: PBGRAPixel; bgra: TBGRABitmap; begin bgra := TBGRABitmap.Create(FastBitmap.Size.X,FastBitmap.Size.Y); with FastBitmap do for Y := 0 to Size.Y - 1 do begin P := PInteger(bgra.ScanLine[Y]); for X := 0 to Size.X - 1 do begin PInteger(P)^ := (Pixels[X, Y] * $010101) or $ff000000; // It is a shortcut for : // P^.Red := Pixels[X, Y]; // P^.Green := Pixels[X, Y]; // P^.Blue := Pixels[X, Y]; // P^.Alpha := 255; Inc(P); end; end; bgra.InvalidateBitmap; // Changed by direct access bgra.Draw(Canvas, 0, 0, False); bgra.Free; end;
We can also use TBGRABitmap only. This library works if possible with device independent bitmaps of the operating system, so it is generally a direct pixel access or quasi-direct pixel access.
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.
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;
RawImage.Description values examples on various platforms:
Platform | Format | Depth | BitsPerPixel | BitOrder | ByteOrder | LineOrder | LineEnd | RedPrec | RedShift | GreenPrec | GreenShift | BluePrec | BlueShift | AlphaPrec | AlphaShift |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Windows | RGBA | 15 | 16 | ReverseBits | LSBFirst | TopToBottom | DWordBoundary | 5 | 10 | 5 | 5 | 5 | 0 | 0 | 0 |
Windows | RGBA | 24 | 24 | ReverseBits | LSBFirst | TopToBottom | DWordBoundary | 8 | 16 | 8 | 8 | 8 | 0 | 0 | 0 |
Linux GTK2 | RGBA | 24 | 32 | BitsInOrder | LSBFirst | TopToBottom | DWordBoundary | 8 | 16 | 8 | 8 | 8 | 0 | 0 | 0 |