| Tech Note |
Manipulating Pixels With Delphi's ScanLine Property
by Earl F. Glynn
First appeared in May 1998 Delphi Developer,
pp. 1-5, 9
Expanded to include information about all PixelFormats.
Modified and expanded to refer to Lab Reports in efg's Computer
Lab.
Contents
Introduction
Scanline and PixelFormat
Additional Examples of Using Scanline
Misuse of Scanline
Optimization
Unexpected Problems Using Scanline
French
Translation by Nono40
(Traduction) ![]()
Chinese
Translation by Hector Xiang
The ScanLine property, new in Delphi 3, allows quick access to individual pixels, but you must know what PixelFormat you're working with before you can access the pixels correctly. In this Tech Note I will demonstrate how to use the ScanLine property to manipulate pixels for a variety of image processing and other applications. Each of the various PixelFormats is first discussed with some accompanying examples. After the "theory" about Scanlines, several applications, mostly using pf24bit Scanlines, are described. Finally, a few unexpected problems using Scanline are described.
How
to Use Scanlines by Leonel Togniolli
Intended to be a simpler introduction to scanlines
Delphi 1and 2 provided a Pixels property for direct access to pixels on a canvas. But accessing pixels using this property is very slow. For example, Listing 1 shows how to rotate a bitmap by 90 degrees using the Pixels property. This works fairly well for small bitmaps as shown in the RotatePixels Lab Report but is unacceptably slow for larger bitmaps. The Pixels property, however, did allow treatment of all sizes of pixels (1 bit to 24 bits) in the same way.
| Listing 1. Rotating a bitmap using the Canvas.Pixels property |
| WITH ImageFrom.Canvas.ClipRect DO BEGIN FOR i := Left TO Right DO FOR j := Top TO Bottom DO ImageTo.Canvas.Pixels[j,Right-i-1] := ImageFrom.Canvas.Pixels[i,j] END; |
(Note: I follow a convention suggested by Jeff Duntemann to capitalize all keywords in Delphi code that I write. I leave code Delphi writes untouched in lowercase. This capitalization convention was also very common back in the '80s with Pascal to emphasize structure in code -- it is not "shouting" as some may think.)
In Delphi 1 and 2 the alternative to the Pixels property is to use Windows API calls to access the pixel data directly (such as GetDIBits). But accessing pixel data in a DIB (device independent bitmap) using API calls is more than just a little complicated. Once you have the DIB data, you often must get it back into a TBitmap for display in a TImage. Delphi 3's ScanLine and PixelFormat properties of a TBitmap provide a nice alternative.
A Delphi ScanLine contains the pixel data, but before accessing the pixel data you must understand the ScanLine's layout in memory using the PixelFormat property. Possible PixelFormats defined in GRAPHICS.PAS include pfCustom, pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, and pf32bit.
Kylix Note: Kylix (K1 - K3) only supports these PixelFormats: pf1bit, pf8bit, pf16bit, pf32bit, pfCustom (defined in QGraphics.pas). In Kylix Scanlines operate on Qimage not on QPixmaps. The first time you call scanline the pixmap is converted to a QImage, which is a bit of a time hit. See UseNote Post by Mattias Thoma about this conversion.
For all PixelFormats, each Scanline of data is padded to the nearest doubleword boundary.
pfCustom Bitmaps
When Delphi cannot
determine the PixelFormat of a bitmap, Delphi assumes the format is pfCustom.
If you try to assign a PixelFormat of pfCustom, an EInvalidGraphic
exception is raised. [See Colin Wilson's UseNet
Post about cases involving pfCustom.]
pfDevice Bitmaps
If you create a TBitmap
and DO NOT assign either a PixelFormat or the HandleType, a
device-dependent bitmap (DDB) is created with a PixelFormat of pfDevice.
The HandleType will be
bmDDB. If you set HandleType := bmDIB, or if you specify any PixelFormat (other than pfDevice),
you get a Device Independent Bitmap (DIB).
pf1bit Bitmaps
You can save a lot of memory if you only need a single bit per pixel
(such as with various masks), but you may need to brush up on bit manipulations in Delphi
to access the pixels. With only a single bit of information per pixel, the normal
"colors" in a pf1bit bitmap are black and white. Alternately, a
two-color palette can be defined to display any two colors.
Use a TByteArray, which is defined in the Delphi SysUtils.PAS unit, to access a pf1bit ScanLine:
| pByteArray = ^TByteArray; TByteArray = ARRAY[0..32767] OF BYTE; |
The width of a pf1bit Scanline in bytes is Bitmap.Width DIV 8 if the width is a
multiple of 8. If width is not a multiple of 8, use the following to calculate the number
of bytes: 1 + (Bitmap.Width-1) DIV 8.
Using the TByteArray from SysUtils limits a pf1bit Bitmap to be
no larger than 32768-by-32768 pixels, which isn't much of a restriction since this would
require more memory than is available in many Windows machines at present.
(Actually, very large bitmaps are usually a problem in Windows 95/98. Size
limitations in Windows NT/2000 are not as severe. See the Very
Large Bitmap Lab Report for details.) I like this construct better than the
Borland solution of an ARRAY[0..0] with range checking required to be turned off
for all variables (See Borland FAQ 890D for
an example of using this construct with what I consider are some awkward conditional
compilation variables.)
The pf1bit Lab Report demonstrates how to create and work with a pf1bit bitmap, including how to attach two-color palettes. In this Lab Report, the button tags for the Black (0), White (1). and Stripes buttons are $00, $FF and $55 (01010101 binary). When these buttons are pressed, each byte of each ScanLine is assigned this tag value. Display the Bitmap by assigning it to ImageBits.Picture.Graphic. See Listing 2.
| Listing 2. Filling a pf1bit bitmap with a constant mask |
| procedure TFormPf1bit.ButtonTagFillClick(Sender: TObject); VAR Bitmap: TBitmap; i : INTEGER; j : INTEGER; Row : pByteArray; Value : BYTE; begin // Value = $00 = 00000000 binary for black // Value = $FF = 11111111 binary for white // Value = $55 = 01010101 binary for black & white stripes Value := (Sender AS TButton).Tag; Bitmap := TBitmap.Create; TRY WITH Bitmap DO BEGIN Width := 32; Height := 32; // Unclear why this must follow width/height to work correctly. // If PixelFormat precedes width/height, bitmap will always be black. PixelFormat := pf1bit; IF CheckBoxPalette.Checked THEN Bitmap.Palette := GetTwoColorPalette END; 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; ImageBits.Picture.Graphic := Bitmap FINALLY Bitmap.Free END end; |
Danny Thorpe's (Borland R&D) clarification about "Unclear why..." comment above in Listing 2:
The superficial answer is that the sequence changes when the bitmap handle gets created -- as soon as the bitmap has a non zero width and height, it will create a handle. Setting the PixelFormat after width and height causes a copy to a new handle of the requested pixel format. Setting the PixelFormat before width and height stores the info until the handle is actually created.
That'd be fine if it weren't for the mono DIB creation code referring to an uninitialized field in the DIB section structure... SrcDIB.dsbm.bmBits = nil is not the case when the internal routine CopyBitmap is called to create a bitmap from scratch (source handle = 0, SrcDIB uninitialized). This will be fixed in D6.
To create the "g" shown in the pf1bit Lab Report, or to create the arrow, bytes from a constant are assigned to the ScanLines. The following shows the bytes used to create the first two ScanLines of the "g" bitmap:
| $00, $FC, $0F, $C0 $07, $FF, $1F, $E0 00000000 11111100 00001111
11000000 |
See the ButtonGClick routine for full details.
The Invert button performs a bit-by-bit inversion of the bitmap. The loops that inverts each ScanLine work as follows:
| Listing 3. Inverting a pf1bit Bitmap |
| FOR j := 0 TO Bitmap.Height-1 DO BEGIN RowOut := pByteArray(Bitmap.Scanline[j]); RowIn := pByteArray(ImageBits.Picture.Bitmap.Scanline[j]); FOR i := 0 TO (Bitmap.Width DIV BitsPerPixel)-1 DO BEGIN RowOut[i] := NOT RowIn[i] END END; |
A pf1bit bitmap has a palette! See the pf1bit Lab Report for details.
For a pf1bit bitmap, after a TBitmap is created, its Width and Height must be specified before the PixelFormat. If not, the pf1bit bitmap will display only in black (This pf1bit Scanline Enigma was described in a UseNet Post.)
pf4bit Bitmaps
Working with pf4bit bitmaps is complicated by the fact that
each pixel is a nibble within a byte. So like with pf1bit bitmaps, bit
manipulations are needed to work with pf4bit bitmaps. With 4 bits of information
per pixel, 16 colors are possible. The "standard" 16 VGA colors are often used,
but palettes can be a complication of working with pf4bit bitmaps.
For a simple example of a pf4bit bitmap, take a look at the pf4bit sample code.
To see how a pf4bit bitmap, defined from an array of constants, can be
used as a brush, see the Brush Bitmaps
Lab Report.
For a more informative example, take a look at the Combine pf4bit Bitmaps Lab Report,
which shows how to create a pf8bit or pf24bit bitmap from two pf4bit
bitmaps. See part of this example in the next section about pf8bit bitmaps.
pf8bit Bitmaps
Working with pf8bit bitmaps is very easy, since each pixel
is a byte and can be accessed directly in a TByteArray. Listing 4 shows the
nested FOR loops necessary to assign all the byte pixels values in a pf8Bit
bitmap. (The code shown in Listing 4 is part of the CycleColors Lab Report, which will be
discussed later.)
| Listing 4. Assigning values to a pf8bit Bitmap using a TByteArray |
| VAR i : INTEGER; j : INTEGER; Row: pByteArray . . . FOR j := 0 TO BitmapBase.Height-1 DO BEGIN Row := BitmapBase.ScanLine[j]; FOR i := 0 TO BitmapBase.Width-1 DO BEGIN Row[i] := <pixel value 0..255>; // assign palette index END END; ImageShow.Picture.Graphic := BitmapBase; |
While working with 8-bit bitmaps with Scanline is easy, the byte value assigned to the Scanline represents the color of the pixel only indirectly. The Scanline value represents the index into the palette table. The palette contains the actual R, G and B components of the color.
I have used pf8bit bitmaps in memory out of addressing convenience, but I usually use pf24bit bitmaps so I can control the displayed colors more easily than working with the complexity of palettes in Windows (see example below).
Listing 5 shows how to transfer the Scanline values from a pf4bit Bitmap to a pf8bit Bitmap. See the Combine pf4bit Bitmaps Lab Report for details about how to form a combined palette for the pf8bit bitmap from the two palettes of the original pf4bit bitmaps.
| Listing 5. Transferring pf4bit
Scanline to a pf8bit Scanline (palette implications are ignored here) |
| // Given a pf4bit Bitmap, Bitmap4, transfer the Scanline
data to a // pf8bit Bitmap, Bitmap8. VAR Bitmap4: TBitmap; // pf4bit Bitmap Bitmap8: TBitmap; // pf8bit Bitmap i : INTEGER; j : INTEGER; Row4 : pByteArray; // pf4bit Scanline Row8 : pByteArray; // pf8bit Scanline ... Bitmap8 := TBitmap.Create; TRY Bitmap8.Width := Bitmap4.Width; Bitmap8.Height := 2 * Bitmap4.Height; Bitmap8.PixelFormat := pf8Bit; // Copy pf4bit Scanlines to pf8bit Scanline FOR j := 0 TO Bitmap4.Height-1 DO BEGIN Row4 := Bitmap4.Scanline[j]; // "input" Scanline Row8 := Bitmap8.Scanline[j]; // "output" Scanline // Width[Bytes] = Width[Pixels] / 2 for pf4bit Bitmap // Assume Width is even number FOR i := 0 TO (Bitmap4.Width DIV 2)-1 DO BEGIN Row8[2*i ] := Row4[i] DIV 16; Row8[2*i+1] := Row4[i] MOD 16 END END; Image8.Picture.Graphic := Bitmap8; FINALLY Bitmap8.Free END |
Consider another pf8bit (and pf24bit) example: How to use GetPaletteEntries and Convert pf8bit Bitmap to a pf24bit Bitmap Using Scanline. This example is intended to be instructional, since normally converting from a pf8bit to pf24bit bitmap is as simple as assigning a new value to the PixelFormat property.
In-memory pf8bit bitmaps. You can create a pf8bit bitmap in memory, along with its palette, but the appearance of this bitmap may depend on the display mode. If you're using high color (15- or 16-bit color) or true color (24- or 32-bit color) display modes, you can define and see all 256 palette entries in a pf8bit bitmap, as shown in Figure 1.
Figure 1. 256 shades of gray in pf8bit bitmap with
high-color display |
|
However, displaying this same pf8bit bitmap in 256-color mode will result in the loss of 20 of the 256 palette entries as shown in Figure 2.
Figure 2. 256 shades of gray in pf8bit bitmap with
256-color display |
Windows "takes" 20 of the 256 colors, normally the first 10 and the last 10, for display of panels, buttons, icons, etc. in 256-color mode. The remaining 236 palette entries can be used by an application.
| Figure 3. Reserved colors in Windows and 236 shades-of-gray palette |
This D3/D4/D5 pf8bit palette example can be downloaded from here for your experimentation. Be careful when creating pf8bit bitmaps and their palette entries unless you absolutely know the bitmaps will be displayed in a true color display mode.
See UseNet Posting about how to create TBitmap with 256 shades of gray.
See Dean Verhoeven's E-mail about using SetDIBColorTable to alter the palette of a pf8bit bitmap.
For additional information about palettes, consult Section B, Color, of the Delphi Graphics Algorithms page.
pf15bit and pf16bit Bitmaps
Not all PixelFormats may be available on every machine. For
example, pf15bit bitmaps are not possible on some machines because of
limitations in the video adapters/video drivers. The following check is
one way to verify whether your machine supports pf15bit bitmaps:
| BEGIN bitmap := TBitmap.Create; TRY bitmap.Width := 32; bitmap.Height := 32; bitmap.PixelFormat := pf15Bit; IF
bitmap.PixelFormat <> pf15bit |
If you're in doubt, you need to verify the correct PixelFormat was created. Apparently the PixelFormat is set to pfCustom if the requested PixelFormat is not honored. See Robert Rossmair's UseNet Post about a possible workaround to this problem. Also see Colin Wilson's UseNet Post about pf15bit/pf16bit cases involving pfCustom.
Use the pWordArray type defined in SysUtils to access a Scanline of pf15bit or pf16bit pixels. Using this type restricts bitmaps to be no larger than 16384-by-16384 pixels, but this isn't much of a limitation since creating a bitmap that size will exhaust system resources, especially on Windows 95/98 machines.
The logical bit layout within a word for each pixel is
pf15bit: 0 rrrrr ggggg bbbbb
pf16bit: rrrrr gggggg bbbbb
A pf15bit pixel has 5-bits of R, G and B, but a pf16bit pixel has an extra bit for the color green -- 6 bits instead of just 5. Green has an extra bit since the eye is more sensitive to green than red or blue.
Listing 6 shows how to create a pf15Bit bitmap filled with yellow pixels. Note that pixels with red=31, green=31 and blue=0 are yellow.
| Listing 6. Creating a "Yellow" pf15bit Bitmap |
| VAR Bitmap: TBitmap; i : INTEGER; j : INTEGER; R : 0..31; // each RGB component has 5 bits G : 0..31; B : 0..31; RGB : WORD; Row : pWordArray; // from SysUtils ... Bitmap := TBitmap.Create; TRY Bitmap.Width := Image1.Width; Bitmap.Height := Image1.Height; Bitmap.PixelFormat := pf15bit; R := 31; // Max Red G := 31; // Max Green B := 0; // No Blue // "FillRect" using Scanline // The bits in each pixel (WORD) are as follows: // 0rrrrrgggggbbbbb. The high order bit is ignored. RGB := (R SHL 10) OR (G SHL 5) OR B; // "yellow" FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO Row[i] := RGB END; Image1.Picture.Graphic := Bitmap FINALLY Bitmap.Free END |
Also see the similar pf16bit example with R := 31 but G := 63.
When converting each 5-bit color component in a pf15bit bitmap to an 8-bit color component in a pf24bit bitmap, what happens with the "extra" 3 bits? See efg's UseNet Post about this. The conversion process is NOT deterministic. The results appear to depend on the video adapter.
According to a UseNet Post by Ian Martin, Photoshop has a problem reading pf16bit bitmaps created by Delphi. Ian's solution was to used pf24bit bitmaps instead.
Paul Nicholls' UseNet Post about accessing a pf16bit Scanline using BASM.
pf24bit Bitmaps
For pf24bit bitmaps, I define (I wish Borland would, too)
the following in Listing 7, which is similar to the TByteArray type:
| Listing 7. TRGBTripleArray Definition |
| CONST PixelCountMax = 32768; TYPE pRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = ARRAY[0..PixelCountMax-1] OF TRGBTriple; |
Using a large value for PixelCountMax serves two purposes. No bitmap can be that large (at present), and this effectively turns off range checking on Scanline variables.
While the definition in Listing 7 puts some bounds on the range of pixel index values (and you might even pick a smaller PixelCountMax), the following is perhaps even a simpler definition:
| TYPE TRGBTripleArray = ARRAY[WORD] OF TRGBTriple; |
Borland defines a TRGBTripleArray in the Graphics.PAS unit, but it is an ARRAY[BYTE] of TRGBTriple.
Danny Thorpe's (Borland R&D) comments:
That's because that array type is for convenience when dealing with color palettes. It is not intended for pixel access. You'll notice that in the evolution from D1 to D4, I've eliminated a lot of temporary memory allocations in the bitmap routines. One of the big timesavers was not allocating heap memory for temporary color palettes. Instead I just use a local variable on the stack of the maximum possible size I'll need: 256 RGBTriples. That burns between 700 and 1k bytes of stack space, but stack is cheap in Win32 and it will be recycled in a few nanoseconds.
That wouldn't work with a TRGBTripleArray type with a WORD range. Even if it didn't blow the stack, it'd still be prohibitively expensive.
The use of Borland's TRGBTripleArray type will cause problems even for "normal" sized bitmaps since a Byte value must range from 0 to 255. The definition I use above does not have this limitation.
The Borland definition (in the Windows unit) for a TRGBTriple follows:
| pRGBTriple = ^TRGBTriple; TRGBTriple = PACKED RECORD rgbtBlue : BYTE; rgbtGreen: BYTE; rgbtRed : BYTE; END; |
Listing 8 shows how to create a pf24Bit array filled with yellow pixels. Note that pixels with red=255, green=255and blue=0 are yellow.
| Listing 8A. Creating a "Yellow" pf24bit Bitmap |
| VAR i : INTEGER; j : INTEGER; Row: pRGBTripleArray; ... Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf24bit; Bitmap.Width := ImageRGB.Width; Bitmap.Height := ImageRGB.Height; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH Row[i] DO BEGIN rgbtRed := 255; // yellow pixels rgbtGreen := 255; rgbtBlue := 0; END END END; // Display on screen ImageRGB.Picture.Graphic := Bitmap; FINALLY Bitmap.Free END |
In Delphi 4.02 or later, this somewhat simpler approach will work using a CONST TRGBTriple:
| Listing 8B. Using a CONST TRGBTriple |
CONST
Yellow: TRGBTriple =
(rgbtBlue: 0; rgbtGreen: 255; rgbtRed: 255);
VAR
i : INTEGER;
j : INTEGER;
Row: pRGBTripleArray;
...
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf24bit;
Bitmap.Width := ImageRGB.Width;
Bitmap.Height := ImageRGB.Height;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
Row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
Row[i] := Yellow
END
END;
// Display on screen
ImageRGB.Picture.Graphic := Bitmap;
FINALLY
Bitmap.Free
END
|
How can a TColor be converted to a TRGBTriple?
FUNCTION ColorToRGBTriple(CONST Color: TColor): TRGBTriple;
BEGIN
WITH RESULT DO
BEGIN
rgbtRed := GetRValue(Color);
rgbtGreen := GetGValue(Color);
rgbtBlue := GetBValue(Color)
END
END {ColorToRGBTriple};
|
Be aware that a TColor can have several internal formats and the above assumes a format of $00bbggrr, which can be created using the RGB function.
How can a pf24bit bitmap be converted to a pf15bit bitmap? Let's consider the "hard way" using Scanline and the "easy way" using PixelFormat. See Listing 9.
| Listing 9. Conversion of pf24bit bitmap to pf15bit |
| VAR Bitmap15: TBitmap; Bitmap : TBitmap; i : INTEGER; j : INTEGER; Row15 : pWordArray; Row24 : pRGBTripleArray; ... Bitmap := TBitmap.Create; TRY Bitmap.LoadFromFile('N:\Images\Flowers\Tulip3.BMP'); // Convert pf24bit bitmap to pf15bit bitmap the "hard way" Bitmap15 := TBitmap.Create; TRY Bitmap15.Width := Bitmap.Width; Bitmap15.Height := Bitmap.Height; Bitmap15.PixelFormat := pf15bit; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row15 := Bitmap15.Scanline[j]; Row24 := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH Row24[i] DO Row15[i] := (rgbtRed SHR 3) SHL 10 OR (rgbtGreen SHR 3) SHL 5 OR (rgbtBlue SHR 3) END END; Bitmap15.SaveToFile('Tulip3-15A.BMP'); FINALLY Bitmap15.Free END; // Convert pf24bit bitmap to pf15bit bitmap the "easy way" ASSERT(Bitmap.PixelFormat = pf24bit); // verify pf24bit Bitmap.PixelFormat := pf15bit; // Assignment results in conversion Bitmap.SaveToFile('Tulip3-15B.BMP') FINALLY Bitmap.Free END // Tulip3-15A.BMP and Tulip3-15B.BMP should be identical bitmaps |
In the example above, the two bitmaps are identical on a pixel-by-pixel basis, but they have different CRC-32 values -- so the files are not completely identical. Perhaps, some "header" value is slightly different in the two files.
A better approach in reducing 24-bit pixels to 15-/16-bit pixels is given in Phil McRevis' UseNet Post where he suggests using Floyd-Steinberg dithering.
Other examples:
How to create a Bitmap
from numeric data? (D5)
efg's UseNet Post about how to flip a TBitmap by writing Scanlines to a MemoryStream and loading the Scanlines of a second TBitmap in the flipped order.
pf32bit Bitmaps
Analogous to the TRGBTriple, define a TRGBQuadArray
to work with pf32bit bitmaps. See Listing 10.
| Listing 10. TRGBQuadArray Definition |
| CONST PixelCountMax = 32768; TYPE pRGBQuadArray = ^TRGBQuadArray; TRGBQuadArray = ARRAY[0..PixelCountMax-1] OF TRGBQuad; |
A simpler definition that eliminates the need for PixelCountMax would be:
| TYPE TRGBQuadArray = ARRAY[WORD] OF TRGBQuad; |
The Borland definition (in the Windows unit) for a TRGBQuad follows:
| pRGBQuad = ^TRGBQuad; TRGBQuad = PACKED RECORD rgbBlue : BYTE; rgbGreen: BYTE; rgbRed : BYTE; rgbReserved: BYTE END; |
As far as color information is concerned, note that the TRGBQuad is equivalent to the TRGBTriple -- both records provide for 24-bits of color information with 8 bits for Red, 8 bits for Green and 8 bits for Blue. The TRGBQuad had an extra rgbReserved field, for what is sometimes called the "alpha" channel (especially in the Mac world). The alpha channel is used by some applications to pass grayscale mask information, but there is no consistent definition of what the alpha channel may contain. NOTE: the contents of the alpha channel are in general unpredictable. See this UseNet post by Stan that "you NEVER know the state of the alpha channel byte."
Danny Thorpe's (Borland R&D) comments:
NT supports (nearly) arbitrary arrangements of RGB bits. The only limitation is the bits of each color element must be contiguous. It would be trivial to load a big-endian RGB file (like Sun's RAW or whatnot) by simply creating the DIBSection with BGR masks. Just for grins, I once created a 32bpp DIBSection with 24 bits of color info, then created another DIBSection using the same pixel memory buffer but defining the pixels in the "alpha channel" reserved byte of the 32bpp data.
One novel use of a pf32bit Scanline would be to use a TBitmap as a dynamic array of Single values from numerical calculations. Such large arrays could be saved to disk with the Bitmap SaveToFile method or loaded from disk with the LoadFromFile method. This numeric array can even be "displayed" in a TImage, where each pixel is the floating-point fraction -- the rgbReserved part of the Single is the exponent of the floating-point number and this would not normally affect the image. Images created this way from scientific/engineering calculations can show some very interesting patterns. See an example of using a pf32bit bitmap to hold and display a matrix of IEEE "single" floating-point values in the Lyapunov Exponents Lab Report. The Fractals Show 2 Lab Report shows how to use a pf32bit bitmap as a matrix of 4-byte integers.
In limited tests, the use of pf32bit Scanlines was about 5% faster than pf24bit Scanlines in performing an identical task. This is probably due to alignment of pf32bit values on 32-bit word boundaries. So using the 3-bytes/pixel pf24bit Scanlines does involve some extra overhead, but usually not enough given the space savings of pf24bit over pf32bit Scanlines.
Ken Florentino's example of using pf32bit bitmaps with assembly language.
efg's UseNet Post about "pf32bit enigma: 'alpha byte' indeterminate in new bitmap?"
efg's UseNet Post about typecasting a shifted TColor to a TRGBQuad:
VAR Color : TColor; yellow: TRGBQuad; .. Color := clYellow; yellow := TRGBQuad(Color SHL 8); |
So starting with a TColor with an internal format of $00bbggrr, if you shift this 8 bits left, you get $bbggrr00, which matches the internal format of a TRGBQuad. A typecast then allows conversion of the shifted TColor to a TRGBQuad.
Kylix Note: efg's UseNet Post with Kylix pf32bit example
PixelFormat Conversion
Assign a new PixelFormat to convert from
one PixelFormat to another. This works well if converting from a lower
bits/pixel to a higher bits/pixel, or when converting between PixelFormats that
do not have any palette implications, e.g., pf15bit to/from pf24bit.
But assigning a new PixelFormat does nothing to ensure you have the
correct palette if you need one.
A pf24bit bitmap can have thousands of colors. For example, the Mandrill monkey bitmap is an array of 512-by-512 = 262,144 pixels, but has 230,427 unique RGB colors! (The Show Image utility gives the number of unique colors in an image -- look in the lower left corner of the display.) In 256-color (pf8bit) display mode, Windows normally reserves 20 of the colors for display of buttons, panels, icons, etc., leaving only 236 colors to an application. To display the Mandrill monkey image, an algorithm is needed to pick the best 236 colors of the 230,427 colors in the image.
See the Show Demo One Lab Report for an algorithm that creates a palette to display a 24-bits/pixel image in 256-color mode. What this example does not do, is to take each TRGBTriple in the pf24bit Scanline, and lookup the "nearest" color in the palette -- it does not convert a pf24bit bitmap to a pf8bit bitmap.
You're extremely lucky if all your pf24bit images display correctly in 256-color mode.
Other Scanline Notes
Question (Paul Nicholls's UseNet Post): "If I wanted to store the pointers to some ScanLines from a bitmap into some variables, when would I have to update these variables to have the pointers be valid again?"
Answer (Steve Schafer's UseNet Post ): "I don't think it's ever really safe to keep a copy of a ScanLine pointer. Even if it were safe to do so under some circumstances in one version of Delphi, it might not be safe under those same circumstances in the next."
"Obtain a ScanLine pointer, use it immediately, and then throw it away."
Peter Haas' UseNet Post about using Bitmap.Dormant after setting PixelFormat to fix a problem in D1-D4 with the TBitmapInfoHeader
Finn Tolderlund's UseNet Post about Scanline and FreeImage
Eric Sibert's pf24bit and pf32bit examples (in French)
http://esibert.developpez.com/kylix/migration/image/image.htm
Matthijs Laan's UseNet Post with MMX example to XOR bitmaps
Andrew Rybenkov's UseNet Post about using GetDIBits/SetDIBits as an alternative to Scanline
Danny Thorpe's UseNet Post about Bilinear Interpolation and Scanline
Additional Examples of Using Scanline
The Daisy Program
The DAISY program uses the same
technique as shown in Listing 8 to create a pf24bit image. The details in the DrawDaisy
method show how this image is created. Briefly, the red and green planes of this image
contain all the variations of these colors. The blue plane contains the "daisy"
with only the brightest values of blue. You will need an 800-by-600 screen, and 15-bit or
higher color for this application.
Displaying a 24-bit true color image is easy in 15-bit or higher color video modes since Windows only has palettes with 256 color or lower video modes. If you try to display a 24-bit bitmap with only 256 colors, you are at the mercy of the current palette in Windows. (See the Show Demo One Lab Report for an alternative.)
The Split Program
The SPLIT program is a
simple image processing program for studying the color planes of an image. The SPLIT
program reads the DAISY.BMP file created by the DAISY program or any other 24-bit color BMP file.
Pressing the speed buttons along the left side results in the display of the corresponding
red, green and blue color planes.
The SPLIT programs stores the original image in a BitmapRGB bitmap and keeps it available as the base image for creating the other bitmaps.
When the Monochrome Checkbox next to the RGB Composite button is checked, each red, green and blue component of the pixel is assigned the same intensity, where Intensity is defined as (R + G + B) / 3. The resulting bitmap, BitmapGray, is assigned back to Image.Picture.Graphic for display. See Listing 11.
| Listing 11. MakeShadesofGrayImage from RGB Composite Image |
| PROCEDURE
TFormSplit.MakeShadesOfGrayImage; VAR Gray : INTEGER; i : INTEGER; j : INTEGER; rowRGB : pRGBTripleArray; rowGray: pRGBTripleArray; BEGIN Screen.Cursor := crHourGlass; TRY FOR j := BitmapRGB.Height-1 DOWNTO 0 DO BEGIN rowRGB := BitmapRGB.Scanline[j]; rowGray := BitmapGray.Scanline[j]; FOR i := BitmapRGB.Width-1 DOWNTO 0 DO BEGIN // Intensity = (R + G + B) DIV 3 WITH rowRGB[i] DO Gray := (rgbtRed + rgbtGreen + rgbtBlue) DIV 3; WITH rowGray[i] DO BEGIN rgbtRed := Gray; rgbtGreen := Gray; rgbtBlue := Gray END END END; FINALLY Screen.Cursor := crDefault END END; |
Other "better" methods of creating a gray scale can be used. See the Spectra Lab Report for two alternatives involve "Y" -- the gray scale used to convert color information (YUV/YIQ) for display on black-and-white TV sets.
To display the Red plane, the red pixel values (that is, the rgbtRed values) are assigned to another bitmap, namely BitmapR, but the pixel values for the blue and green components are assigned a 0 value. When the Monochrome Checkbox is checked, the red pixel values are assign to rgbtRed, rgbtBlue and rgbtGreen resulting in a "shades of gray" intensity map.
The explanation of the conversion of RGB (Red-Green-Blue) to HSV (Hue-Saturation-Value) is in the HSV Lab Report. An excellent book about color conversions (and anything about Computer Graphics) is Computer Graphics Principles and Practice by Foley, et al, Addison-Wesley, 1996. Or, take a look at the general Color Information in efg's Reference Library. For Delphi color information look at Section B, Color, of the Delphi Graphics Algorithms page.
Rotation of an Image
As discussed earlier, rotation of a bitmap is too slow with the canvas' Pixels
property. But the use of the ScanLine property makes rotation of a bitmap any
angle fairly fast. The Rotate Scanline Lab Report
shows that the rotation of the 640-by-480 pf24bit bitmap degrees clockwise takes slightly
more than a second on a 166 MHz Pentium.
Each pixel is not rotated to a new position in a "forward" direction. You start with the rotated image and consider where each pixel was in the original image. You rotate "back" and take the closest pixel from the original image. Because of the integer math in the rotation, certain artifacts can be introduced with a rotation. Usually anti-aliasing isn't necessary when rotating 24-bit images of most objects. Anti-aliasing may be necessary if rotating thin lines or text, however.
Also see the Flip/Reverse/Rotate Lab Report.
Color Cycling in a pf24bit Bitmap
The old VGA palette tricks won't work with 24-bit color images. Remember:
Windows only uses palettes with 256 color display modes or lower. Palettes are not used
with high color (15 or 16-bit color) or true color (24-bit color) video modes.
When Windows uses palettes, you only have 236 colors available for your application since usually the bottom 10 and top 10 colors of the palette are fixed by Windows. Even though this technique of color cycling with a pf24bit bitmap is a little slow without using assistance from the hardware, the Color Cycle Lab Report, uses a lookup table of 1280 colors (5*256), which is a far greater number than in a normal Windows palette.
The FormCreate method defines the entries in the ColorCycle ARRAY OF TRGBTriples. The first 256 colors are shades of red, followed by 256 shades of green and 256 shades of blue. The fourth set of 256 is somewhat similar to a "Fire Storm" palette used by the FractInt fractal program. The fifth set defines shades of gray.
After starting the CycleColors program, the fractal image takes about 90 seconds to create on a 166 MHz Pentium. (The math behind this image is outside the scope of this article.) When the image has completed, the Cycle Colors Checkbox is enabled. This fractal image is a pf8bit bitmap (created much like the method in Listing 4.)
With the Cycle Color checkbox checked, the application's OnIdle hander takes the pf8bit bitmap, BitmapBase, and defines all the RGB pixels in BitmapRGB using the ColorCycle array. See Listing 12 below. Uncheck the Cycle Colors Checkbox to stop the change of colors. While somewhat slow, this color cycling , which is done completely in software, is fairly impressive.
Listing 12. Using IdleAction to Cycle Colors |
| PROCEDURE
TFormColorCycle.IdleAction(Sender: TObject; VAR
Done: BOOLEAN); VAR i : INTEGER; index : INTEGER; j : INTEGER; RowIn : pByteArray; RowRGB: pRGBTripleArray; BEGIN IF NOT CheckBoxCycle.Checked THEN Done := TRUE ELSE BEGIN INC (CycleStart); IF CycleStart >= ColorList.Count THEN CycleStart := 0; LabelCycle.Caption := IntToStr(CycleStart); FOR j := 0 TO BitmapBase.Height-1 DO BEGIN RowIn := BitmapBase.ScanLine[j]; RowRGB := BitmapRGB.ScanLine[j]; FOR i := 0 TO BitmapBase.Width-1 DO BEGIN index := CycleStart + RowIn[i]; IF index >= ColorList.Count THEN index := index - ColorList.Count; RowRGB[i] := pRGBTriple(ColorList.Items[index])^ END END; ImageShow.Picture.Graphic := BitmapRGB; Done := FALSE; END END {IdleAction}; |
Example of image
resampling using Scanline (D3-D5)
Shrink/Flip/Reverse image
example (D3)
Example of comparing two TBitmaps
(D3-D5)
by Danny Thorpe (Borland R&D)
Many people are too quick to use Scanlines[] to perform operations that are better handled by GDI and raster ops. Scanlines[]'s main advantage is that it gives you direct access to the pixels with a minimum of overhead. That doesn't mean that Scanlines[] is the fastest way to manipulate the bitmap contents. Most video cards today contain very sophisticated, purpose-built blit engines that can do raster ops on bitmap data usually much faster than the main CPU. I've seen a video card ROP an entire bitmap faster than the main CPU can finish even one scanline. The CPU can only do one pixel at a time, and each access is a memory reference that could stall the CPU pipeline because of a cache miss. If the DIBSection lives in video memory, the CPU has to cross the PCI or AGP bus to get to it. The video card has none of these restrictions - it has immediate and direct access to low-latency video memory and most blit engines are designed to ROP multiple pixels at once. Filling video memory with zeros or color values (i.e., brush) is also a high priority for video hardware. I've heard that the secret to one high-end 3D video card's extremely high zero fill rate is that instead of filling the RAM with zeros, they actually turn off the power to the RAM banks!
An example: the Listing 3 example of inverting a bitmap by iterating through the pixels and individually NOTting each pixel. I seriously doubt that this will [have the same performance] as a PatBlt(Bitmap.Canvas.Handle, 0, 0, Bitmap.Width, Bitmap.Height, DSTINVERT), even on a plain old VGA frame buffer. Listing 3 is fine as an educational tool, but as we both know from the Delphi documentation that mentions Canvas.Pixels[], people will adopt sample code as gospel, no matter how inappropriate it is for production work. ;>
Same observation for Listing 6: While the function is described as equivalent to "FillRect" with a yellow brush, no mention is made that the FillRect will probably be many times faster or that the example is intended for instruction, not for production.
Similarly, merging paletted bitmaps can be done without pixel manipulations. The trick is to merge the palettes first, assign the new palette to the destination bitmap, and then blit the source bitmaps to the destination. GDI and the video hardware will do the color matching for you.
Also, since D4, assigning a palette handle to a bitmap object will re-vector the bitmap pixels to the nearest available colors in the new palette. D3 and earlier did not do this.
1. Minimizing Scanline Accesses
Several times in the various Delphi newsgroups someone suggests that the number of Scanline accesses should be minimized as an optimization technique since considerable overhead is thought to exist with each call. For example, instead of accessing Scanline once per row (as in many of the examples here), with some extra work Scanline can be accessed only twice at the beginning of the loop stepping through the rows. A "pointer" can be incremented using integer arithmetic instead of a Scanline call for each row. See Listing 13 for an an example.
Listing 13. Minimizing Scanline Accesses |
| procedure TForm1.ButtonOptimizedClick(Sender: TObject); VAR Bitmap : TBitmap; Delta : INTEGER; i : INTEGER; j : INTEGER; k : INTEGER; LoopCount : INTEGER; row : pRGBTripleArray; ScanlineBytes: INTEGER; StartTime : DWORD; // Use DWORD to keep D3 and D4 happy begin LabelOptimized.Caption := ''; LoopCount := SpinEditTimes.Value; StartTime := GetTickCount; FOR k := 1 TO LoopCount DO BEGIN Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf24bit; Bitmap.Width := 640; Bitmap.Height := 480; row := Bitmap.Scanline[0]; ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(row); FOR j := 0 TO Bitmap.Height-1 DO BEGIN FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH row[i] DO BEGIN rgbtRed := k; rgbtGreen := i MOD 256; rgbtBlue := j MOD 256; END END; INC(Integer(Row), ScanlineBytes); END; ImageOptimized.Picture.Graphic := Bitmap FINALLY Bitmap.Free END END; Delta := GetTickCount - StartTime; // ms LabelOptimized.Caption := IntToStr(Delta) + ' Total ms; ' + Format('%.1f', [Delta / LoopCount]) + ' ms/bitmap'; end; |
But how effective is this technique? To determine how effective this technique is, a small D3/D4 program ScanlineTiming was written to compare the "brute force" technique of access the Scanline property for each row of a bitmap with the technique shown above in Listing 13. The results are shown in the following table:
Time[ms] (Mean ± Standard Deviation) Per 640-by-480
Bitmap |
|||
| Pentium Speed [MHz] | Brute Force [ms/bitmap] | Optimized [ms/bitmap] | Savings [ms/bitmap] |
| 120 | 188.3 ± 0.6 | 187.9 ± 0.9 | 0.4 |
| 166 | 70.9 ± 0.07 | 69.8 ± 0.04 | 1.1 |
| 400 | 16.27 ± 0.02 | 15.40 ± 0.00 | 0.87 |
| 450 | 17.69 ± 0.35 | 16.78 ± 0.32 | 0.91 |
In my opinion, this optimization technique is probably the last thing one should worry about while working with a bitmap -- all other algorithms should be scrutinized first. Rarely is saving 1 millisecond per bitmap worth the optimization -- perhaps with some real-time applications this savings would be significant enough. On the faster machines this represents only about a 5% savings.
E-Mail comments from Robert Lee about this "Optimization":
...the utility of this technique is likely to vary depending upon what you are doing to/with the bitmap. For instance, half the time in your example is spent creating and freeing the bitmap itself. If the bitmap(s) in question were already created then the relative effect of the technique would increase to 10%. Also, if the dimensions were changed to something long and skinny, or if the operation involved two or more bitmaps (say a blend) then the effect would get progressively more significant. With a little fooling round I got to a 2x difference just by resizing the bitmap.
My preferred strategy for reporting the impact of optimization techniques is to state both some sort of "best case" number and a "typical" number. Thus the typical number might be 5% (probably even less), but the "best case" effect could be 2-4x." [Thanks, Robert, for the clarification.]
Danny Thorpe's (Borland R&D) comments:
On the question of performance impact of calling Scanlines[] a lot: Yes, there is a potential performance hit. The DIBSection's memory buffer is not guaranteed to be coherent (reflect the most recent GDI operations) unless you call GDIFlush before accessing the pointer. The ScanLines[] property has to call GDIFlush to ensure the buffer contents are in sync. If the bitmap has been modified by GDI calls, then the call to GDIFlush could block waiting for the GDI pipeline to empty. If no modifications are still pending, GDIFlush should return immediately, but there will always be the overhead of making an additional call.
Additionally, TBitmap.GetScanlines() calls TBitmap.Changing, which may have to create a completely new, unshared copy of the image if the bitmap is shared. That will completely blow out performance on the first touch of Scanlines, and it chips away a little even when the bitmap isn't shared.
The technique shown in Listing 13 for minimizing time lost to ScanLines[] was developed by me while writing the TJPEGImage class. There is more at work here than meets the eye, though, which should be mentioned with that listing. The easiest way to eliminate calls to ScanLines[] is to simply do your own pointer arithmetic to advance the start-of-scanline buffer pointer to the next scanline. There are two hazards in doing that: 1) correctly dealing with the DWORD alignment of scan lines, and 2) the physical layout of scanlines in the memory buffer. DIBs can be oriented as "top-down", where the first row of pixels in the bitmap reside in the first bytes of memory in the buffer, or as "bottom-up", where the first row of pixels reside in the last bytes of memory and grow upward in memory. Oddly enough, "bottom-up" is the more common DIB orientation, possibly due to BMP's OS/2 origins.
The technique of subtracting the address of Scanline[0] from the address of Scanline[1] solves both problems very nicely. It gives you the distance between scanlines including DWORD padding, if any, and the sign of the delta implicitly indicates the DIB memory orientation. This eliminates the need for performance-robbing conditional statements in the critical pointer advancement loop. Just increment the pointer by that delta, and it will put you on the next scanline, be it above or below the current address.
Comments from Nono40 (Nov 2003):
I have one note about the "Optimization" (Accessing ScanLines only twice for a bitmap ). We usually use (0,0) at the top left corner of a bitmap. But in that case points like ( Cos(t) , Sin(t) ) will go clockwise.
If we want to use a "normal" drawing with ( Cos(t) , Sin(t) ) going anti-clockwise ( and (0,0) pixel at bottom left ). There is no need to change "y" by "Height-Y-1" each time. There is only to change line 0 pointer and Delta value between two lines:
PtrBmp:=BitMap.ScanLine[BitMap.Height-1]; Delta:=Integer(BitMap.ScanLine[BitMap.Height-2])-Integer(PtrBmp);
Of course, after the "Y" signification is not the same in ScanLine access and Canvas access. But it's not a problem if we use only ScanLine access.
Just a personnal point of view: I prefere pf32bit, as i use assembly language with ScanLine. It's easier to access pixels with a complete Integer, even if 25% of space is lost. With ScanLine if one register olds the ScanLine pointer of one Line ( ex EDI ), and another register olds "X" value ( ex EAX). Read/Write of the pixel is very simple using bulit-un index multiplier:
MOV Dword Ptr [EDI+EAX*4],clYellow
(example : http://nono40.developpez.com/sources/source0069.html)
Please send me feedback if you think I missed something in this timing test or if you have significantly different results.
2. Accessing Scanlines via Assembly Language
Paul Nicholls' UseNet Post about
accessing a pf16bit Scanline using BASM.
Robert Rossmair's UseNet
Post about calculating the average color of a bitmap
Ken
Florentino's example of using pf32bit bitmaps with assembly language.
3. Copying Raw Data to Scanline Area
In a borland.public.delphi graphics post Mikael Stalvik wanted to create an array of 4-byte integers in memory and then fill a pf32bit bitmap with them. I created two examples for Mikael, one "unoptimized" and another "optimized." But the performance of both approaches was nearly the same.
The "unoptimized" approach involved filling the scanlines in increasing order (from top-to-bottom):
|
Listing 14. "Unoptimized" Way to Fill Scanlines from In-Memory Array of Integers |
procedure TForm1.Button1Click(Sender: TObject);
TYPE
TRGBQuadArray = ARRAY[WORD] OF INTEGER;
pRGBQuadArray = ^TRGBQuadArray;
VAR
i,j : INTEGER;
Bitmap: TBitmap;
row : pRGBQuadArray;
Start : DWORD;
begin
Start := GetTickCount;
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf32bit;
Bitmap.Width := 640;
Bitmap.Height := 480;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width-1 DO
BEGIN
row[i] := i*j // something to make interesting bitmap
END
END;
Image1.Picture.Graphic := Bitmap;
ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
FINALLY
Bitmap.Free;
END
end;
|
The "optimized" approach involved a single "Move" operation
of all the in-memory data to the scanline area. [Note: There are
some scanline padding requirements -- each scanline must be a multiple of
8-bytes long -- that are ignored here. Again, the
following assumes a bitmap has the proper width so no padding occurs at the end
of each scanline.]
But what is the memory address of each pf32bit pixel in a TBitmap? Insert the following IF statement inside the above nested loops to find out:
| // Show "corner" addresses of pixels in
bitmap IF ((i=0) OR (i=Bitmap.Width-1)) AND ((j=0) OR (j=1) OR (j=Bitmap.Height-1)) THEN Memo1.Lines.Add('(' + IntToStr(i) + ', ' + IntToStr(j) + ') = ' + InttoHex( Integer(@row[i]),8 )); |
Here are the results from a 640-by-480 pf32bit bitmap:
Addresses of certain pixels in a particular pf32bit TBitmap
| row \ column | 0 | ... | 639 |
| 0 | $8374E600 | ... | $8374EFFC |
| 1 | $8374DC00 | ... | $8374E5FC |
| ... | ... | ... | ... |
| 479 | $83623000 | ... | $836239FC |
Note the largest and smallest address values. The beginning address of the bitmap is the 0th pixel of row 479, and the highest-address pixel is pixel 639 in row 0. So the memory addresses increase left-to-right, but from bottom to top.
But this isn't the only possibly arrangement in a TBitmap. As Alex Denissov pointed out in the newsgroup, when the BitmapInfo.bmiHeader.biHeight value is negative, logical (0,0) is the top-left corner, and if positive, it is bottom-left one. TBitmap normally has (0,0) at the top-left corner.
One easy to find out if the addresses of the scanlines are increasing or decreasing is to compute:
ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(Bitmap.Scanline[0]);
How many bytes are in the bitmap? Compute ($8374EFFC + 4) - $83623000 = $12C000 = 1,228,800 = 4*640*480, which is the expected value.
To use "Move" we need the pointers for the "from" and "to" addresses. The "to" address is the address of the 0th pixel of the last row, as shown below:
| Listing 15. "Optimized" Way to Fill Scanlines from In-Memory Array of Integers |
procedure TForm1.Button1Click(Sender: TObject);
VAR
Bitmap: TBitmap;
x : ARRAY OF Integer;
i,j : INTEGER;
p : ^Integer;
Start : DWORD;
begin
SetLength(x, 640*480);
FOR j := 0 TO 479 DO
FOR i := 0 TO 639 DO
x[640*j + i] := i*j; // something interesting
Start := GetTickCount;
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf32bit;
Bitmap.Width := 640;
Bitmap.Height := 480;
p := Bitmap.Scanline[Bitmap.Height-1]; // starting address of "last" row Move(x[0], p^, Bitmap.Width*Bitmap.Height*SizeOf(Integer)); Image1.Picture.Graphic := Bitmap;
ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
FINALLY
Bitmap.Free;
END
end;
|
GetTickCount was used in both cases to measure how effective the approach was. I couldn't detect a significant difference between these approaches on a 650 MHz Pentium III.
Two final notes:
1. The two methods shown above are not identical. The resulting bitmaps are flipped top-to-bottom, which was ignored above.
2. The "raw" binary integer data above from the computation of "i*j" (from the pixel's coordinates) results in an interesting bitmap.
Unexpected Problems Using Scanline (D3-D4)
1. Using Scanline Only after a TBitmap.Assign
Will Corrupt the Original Bitmap (problem in D3/D4; fixed in
D5)
TBitmap.Assign should be a "deep copy" (all data copied) versus a
"shallow copy" (only a pointer copied). Actually, because of reference
counting, the Assign is a shallow copy until something "significant"
happens at which time an independent copy of the bitmap is made. This can be very
subtle to catch sometimes.
Below, the assignment of a single Pixels value changes the "shallow copy" to a "deep copy." The need for this is either a bug or a bad design.
| BitmapAlign := TBitmap.Create; TRY // Assign doesn't directly make an independent copy of a bitmap. // Problem affects Delphi 3 and 4. BitmapAlign.Assign(Bitmaps[2]); // Force BitmapAlign to be totally independent of Bitmaps[2] // This is an unfortunate kludge needed if Scanline is the only // method to access pixel data. Without this kludge, the // original bitmap and "new" bitmap are operating on the same // pixel data!!! BitmapAlign.Canvas.Pixels[0,0] := Bitmaps[2].Canvas.Pixels[0,0]; <Scanline-only access of pixel data in BitmapAlign here> FINALLY BitmapAlign.Free END |
Here's some sample code that shows this problem: ScreenBitmapAssignEnigma.PAS.
Finn Tolderlund's solution (after digging through the VCL) was to issue this command:
BitmapAlign.Assign(Bitmaps[2]);
BitmapAlign.FreeImage;
<Scanline-only access of pixel data in BitmapAlign here>
The FreeImage has the effect of converting the shallow bitmap copy to a deep bitmap copy.
[Thanks Finn for this tip! --efg, 21 Jan 2001]
2. The pf24bit Scanline
Assignment Enigma (a rare Delphi optimization bug)
This problem appears in Delphi 3 and 4, but appears to be fixed with the Delphi 4 Update
Pack 2 (Build 5.104). Thanks to Marko
Peric for informing me that Delphi 4 Update Pack 2 fixes this problem. [efg, 14
Dec 1998]
The following assignment of a TRGBTriple from one pf24bit Scanline to another will result in an Access Violation if Delphi optimization is enabled:
| VAR BitmapA : TBitmap; BitmapB : TBitmap; i : INTEGER; j : INTEGER; RGBTriple: TRGBTriple; rowInB : pRGBTripleArray; rowInA : pRGBTripleArray; // Define a pf24bit BitmapA BitmapB := TBitmap.Create; TRY BitmapB.Width := BitmapA.Width; BitmapB.Height := BitmapA.Height; BitmapB.PixelFormat := pf24bit; FOR j := 0 TO BitmapA.Height-1 DO BEGIN RowInA := BitmapA.ScanLine[j]; RowInB := BitmapB.Scanline[j]; FOR i := 0 TO BitmapA.Width-1 DO BEGIN RowInB[i] := RowInA[i]; // Access Violation END END; |
The following is the "best" workaround that I've found (since turning optimization off makes too much of a difference in execution speed):
| VAR RGBTriple: TRGBTriple; // Kludge workaround for best performance RGBTriple := RowInA[i]; RowInB[i] := RGBTriple |
See additional details on this problem. Amazingly, Borland claims this is not a bug. They told me: "We determined that your bug report isn't a bug, but might be due to a misunderstanding of how the product works."
3. Scanline code fragment that causes Internal Error C1127 (D4) and C1141 (D5). Borland Bug Case 430492.
4. Problem with complex implementation that allows DDB and DIB Section to exist simultaneously in Single TBitmap as described in UseNet Post by Takuo Nakamura.
Conclusion
This Tech Note covered a number of examples of using Delphi's ScanLine
and PixelFormat properties to manipulate pixels in a variety of ways. Many image
processing and graphics enhancements can be written in Delphi with fairly short routines
using ScanLine. I hope others also discover how useful Delphi's RAD capabilities
are in the area of image processing and computer graphics.
References
Using Device-Independent Bitmaps and Palettes
http://support.microsoft.com/support/kb/articles/q72/0/41.asp
Sample: 16 and 32 Bits-Per-Pel Bitmap Formats
http://support.microsoft.com/support/kb/articles/q94/3/26.asp
Thanks to Danny Thorpe, Senior Engineer, Delphi R&D, for his useful feedback.
Thanks to Gus Elliot for pointing out an omission in Listing 13. (25 Jan 2001)
Updated 16 Nov 2003
since 1 Nov 1998