Nono40.developpez.com
Le petit coin du web de Nono40
SOURCES TESTS DELPHI WIN32 AUTOMATISMES DELPHI .NET QUICK-REPORT

Utilisation de la propriété TBitMap.ScanLine

Ce document est la traduction de la version anglaise de l'auteur disponible ici :
http://www.efg2.com/Lab/ImageProcessing/Scanline.htm

La propriété ScanLine a été introduite avec Delphi 3, elle permet un accès rapide à chaque pixel. Mais vous devez connaître le format de pixel utilisé (TBitMap.PixelFormat ) pour accéder correctement aux pixels. Cet article va montrer comment utiliser la propriété ScanLine pour traiter des pixels dans différentes applications graphiques ou autres. En premier lieu, chaque format de pixel va être décrit, accompagné d'un exemple. Après cette présentation théorique, plusieurs applications seront décrites, la plupart utilisant le format pf24bit. En dernier, quelques problèmes particuliers sur l'utilisation de ScanLine seront présentés.

Article lu   fois.

Les deux auteurs

Site personnel

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Introduction

Delphi 1 et 2 fournissent une propriété Pixels pour accéder à chaque pixel d'un Canvas. Mais cette méthode d'accès est très lente. Par exemple, le code donné ci-dessous montre comment effectuer une rotation de 90° sur un bitmap en utilisant Pixels. Ceci fonctionne bien sur de petits bitmaps (RotatePixels Lab Report) , mais est beaucoup trop long pour des bitmaps plus larges. Par contre, la propriété Pixels permet d'effectuer des traitements sur toutes les tailles de pixels avec le même code.

Listing 1 : Rotation d'un bitmap en utilisant la propriété Canvas.Pixels
Sélectionnez

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;

Avec Delphi 1 et 2, l'alternative à l'utilisation de Pixels est l'appel de fonctions APIs pour accéder aux données des pixels directement ( comme la fonction GetDIBits ). Mais accéder aux données d'un DIB ( Device Independent Bitmap ) est plus compliqué. Un fois que les données de DIB sont créées, il faut souvent les convertir de nouveau en Bitmap pour pouvoir les afficher dans un TImage. Les propriétés ScanLine et PixelFormat de Delphi 3 offrent une alternative intéressante.

I. ScanLine et PixelFormat

ScanLine contient les données des pixels ; mais, avant d'accéder aux pixels, vous devez connaître le format de ScanLine en mémoire en utilisant la propriété PixelFormat. Les valeurs possibles de PixelFormat sont définies dans l'unité GRAPHICS.PAS et sont pfCustom, pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, and pf32bit.

Note pour Kylix : Kylix (K1 - K3) supporte seulement les formats suivants : pf1bit, pf8bit, pf16bit, pf32bit, pfCustom (définies dans QGraphics.pas). Dans Kylix ScanLine est défini sur un QImage et non un QPixMaps. La première fois que vous appelez ScanLine, le Pixmap est converti en QImage, ce qui est une perte de temps.
Voir UseNote Post by Mattias Thoma à propos de cette conversion.

Quel que soit le format de pixel, chaque ligne de données est alignée sur une frontière de double-mot.

I-A. Format pfCustom

Quand Delphi ne peut pas déterminer le PixelFormat d'un BitMap il affecte le format à pfCustom. Si vous essayez d'affecter pfCustom à PixelFormat une exception EInvalidGraphic est générée.
( Voir Colin Wilson's UseNet Post about cases involving pfCustom. )

I-B. Format pfDevice

Si vous créez un TBitmap et que vous n'affectez ni PixelFormt et ni HandleType, un DDB ( Device Dependent Bitmap ) est créé avec un PixelFormat de pfDevice. Le HandleType sera bmDDB. Si vous fixez HandleType à bmDIB, ou si vous fixez PixelFormat ( autre que pfDevice ), vous obtenez un DIB ( Device Independent Bitmap ).

I-C. Format pf1bit

Vous pouvez économiser une grande quantité de mémoire si vous n'avez besoin que d'un seul bit par pixel ( comme les différents masques ), mais vous devez vous faire aux manipulations de bits pour accéder aux pixels. Avec seulement un seul bit d'information pour chaque pixel, les couleurs "normales" d'un bitmap pf1bit sont le noir et le blanc. Une palette de deux couleurs peut tout de même être définie pour afficher deux couleurs quelconques.

Utilisez un TByteArray, défini dans l'unité SysUtils.PAS, pour accéder aux données :

 
Sélectionnez

pByteArray = ^TByteArray;
TByteArray = ARRAY[0..32767] OF BYTE;

La taille en octets d'un ligne de ScanLine pf1bit est BitMap.Width Div 8 si la taille est multiple de 8. Si la taille n'est pas multiple de 8, utilisez la formule suivante pour calculer le nombre d'octets : 1 + (Bitmap.Width - 1) DIV 8.

L'utilisation d'un TByteArray de SysUtils limite la taille d'un Bitmap pf1bit à 32768x32768 pixels, ce qui n'est pas vraiment une restriction, étant donné que ceci demande plus de mémoire que celle disponible dans les ordinateurs actuels. ( Actuellement, les très grands Bitmaps posent un problème sous Windows 95/98. Les limitations sont moindres avec Windows NT/2000. Voir Very Large Bitmap Lab Report for details. ) Je préfère cette construction à celle de Borland qui est un ARRAY[0..0] nécessitant de désactiver le contrôle d'étendue pour toutes les variables. ( Voir Borland FAQ 890D pour un exemple utilisant cette méthode que je considère comme une utilisation maladroite des variables de compilation conditionnelle. )

Le pf1bit Lab Report montre comment créer et travailler avec un Bitmap pf1bit, y compris comment ajouter une palette de deux couleurs. Dans cet essai, les propriétés Tag des boutons noir, blanc et rayé sont respectivement $00, $FF et $55 ( 01010101 en binaire ). Quand un bouton est appuyé, chaque octet de chaque ligne de ScanLine est modifié avec cette valeur. L'affichage est effectué en assignant le bitmap à ImageBits.Picture.Graphic. Voir "listing 2"

Listing 2. Remplir un bitmap pf1bit avec un masque fixe
Sélectionnez

procedure TFormPf1bit.ButtonTagFillClick(Sender: TObject);
  VAR
    Bitmap: TBitmap;
    i     : INTEGER;
    j     : INTEGER;
    Row   : pByteArray;
    Value : BYTE;
begin
  // Value = $00 = 00000000 en binaire pour le noir
  // Value = $FF = 11111111 en binaire pour le blanc
  // Value = $55 = 01010101 en binaire pour les rayures
  Value := (Sender AS TButton).Tag;

  Bitmap := TBitmap.Create;
  TRY
    WITH Bitmap DO
    BEGIN
      Width := 32;
      Height := 32;

      // Etrange, pourquoi cette ligne doit suivre Width/Height pour
      // que le code fonctionne ? Dans le cas contraire, le bitmap
      // est toujours noir.
      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;

Voici la réponse de Danny Thorpe's (Borland R&D) concernant le point "étrange" dans le commentaire du listing 2 :
Une réponse partielle est que la séquence change quand le Handle du bitmap est créé. Dès que le Bitmap a une largeur ou une hauteur non nulle, le Handle est créé. Affecter le PixelFormat après la taille provoque la création d'un nouveau handle avec le format de pixel demandé. Affecter PixelFormat avant la taille, enregistre l'information jusqu'a ce que le handle soit créé.
Ceci serait bien s'il n'y avait pas dans le code de création d'un DIB monochrome une référence à un champ non initialisé de la structure DIB... SrcDIB.dsbm.bmBits = nil n'est pas le cas quand la routine interne CopyBitMap est appelée pour créer un nouveau bitmap ( Source Handle = 0, SrcDIB non initialisé ). Ceci sera corrigé à partir de Delphi 6.

Pour créer le "g" affiché dans pf1bit Lab Report ou pour créer une flèche, les octets d'un tableau de constantes sont copiés dans ScanLine. Les valeurs suivantes sont celles utilisées pour les deux premières ligne du bitmap "g" :

 
Sélectionnez

$00, $FC, $0F, $C0
$07, $FF, $1F, $E0
00000000 11111100 00001111 11000000
00000111 11111111 00011111 11100000

Voir le code du bouton "ButtonG" pour plus de détails.

Le bouton "invert" effectue une inversion bit à bit du bitmap. La boucle qui inverse chaque ligne de ScanLine fonctionne comme suit :

Inverser un bitmap pf1bit
Sélectionnez

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;

Le bitmap pf1bit possède une palette ! Voir pf1bit Lab Report

Pour un bitmap pf1bit, un fois que le Bitmap est créé il faut absolument affecter Width et Height avant PixelFormat. Dans la cas contaire le bitmap pf1bit sera seulement affiché en noir. ( Cette énigme du bitmap pf1bit est décrite dans un post UseNet )

I-D. Format pf4bit

Travailler avec des bitmaps pf4bit est compliqué par le fait que chaque pixel n'est qu'une partie d'un octet. Comme pour les bitmaps pf1bit, il faudra travailler avec les bits pour les bitmaps pf4bit. Avec 4 bits par pixel, il est possible d'utiliser 16 couleurs. Le plus souvent elles correspondent au standard VGA 16 couleurs. Les palettes ne sont qu'une complication supplémentaire en travaillant avec des bitmaps pf4bit.

Voici un exemple simple d'utilisation d'un bitmap pf4bit :

 
Sélectionnez

// Affichage de barre de 16 couleurs dans un bitmap pf4bit
// efg, 6 October 1998

procedure TForm1.ButtonPf4bitClick(Sender: TObject);
  VAR
    Bitmap:  TBitmap;
    i,j   :  INTEGER;
    m     :  INTEGER;
    row   :  pByteArray;    // Chaque pixel est un morceau (1/2 octet )
begin
  Bitmap := TBitmap.Create;
  TRY
    Bitmap.Width  := Image1.Width;
    Bitmap.Height := Image1.Height;  // Doit être un multiple de 16
    Bitmap.PixelFormat := pf4bit;    // 16 couleurs

    FOR j := 0 TO Bitmap.Height-1 DO
    BEGIN
      m := j DIV 16;      // 16 bands :  0 .. 15
      row := Bitmap.Scanline[j];
      FOR i := 0 TO Bitmap.Width DIV 2 - 1 DO  // 2 pixels par octet
      BEGIN
        row[i] := m         + {pixel du morceau bas}
                  (m SHL 4);  {pixel du morceau haut}
      END
    END;

    Image1.Picture.Graphic := Bitmap
  FINALLY
    Bitmap.Free
  END
end;

Pour voir comment un bitmap, défini par un tableau de constantes, peut être utilisé comme un pinceau ( TBrush.Bitmap ) regardez the Brush Bitmaps Lab Report.

Pour un autre exemple, regardez Combine pf4bit Bitmaps Lab Report montrant comment créer un pf8bit ou pf24bit à partir de deux bitmaps pf4bits. Une partie de cet exemple est décrit dans le chapitre suivant sur les bitmaps pf8bit.

I-E. Format pf8bit

Travailler avec un bitmap pf8bit est très facile, chaque pixel occupant un octet ils peuvent être utilisés via un TByteArray. Le listing 4 montre les boucles For imbriquées pour affecter tous les pixels d'un bitmap pf8bit. ( ce code est une partie du code CycleColors Lab Report, qui sera présenté plus loin.)

Listing 4 : Affectation des pixels d'un pf8bit en utilisant TByteArray
Sélectionnez

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>;   // Affecter un index de palette
  END
END;

ImageShow.Picture.Graphic := BitmapBase;

Travailler avec des bitmaps pf8bit est facile, la valeur octet affectée à un point d'une ligne représente la couleur du point mais indirectement. Cette valeur est un index dans une palette de couleur. La palette contient les composantes R, V et B de chaque couleur.

J'ai présenté la méthode d'accès des bitmap pf8bit simplement pour information. Habituellement j'utilise des bitmaps pf24bits permettant de contrôler la couleur d'un point plus facilement qu'avec la complexité des palettes dans Windows. ( voir l'exemple ci-dessous )

Le listing 5 montre comment copier les valeurs d'un bitmap pf4bit vers un bitmap pf8bit. Voir le Combine pf4bit Bitmaps Lab Report pour les détails concernant la méthode pour regrouper les deux palettes des bitmap pf4bit dans la palette du bitmap pf8bit.

Listing 5 : copier les données d'un bitmap pf4bit dans un bitmap pf8bit
Sélectionnez

// A partir d'un bitmap pf4bit nommé Bitmap4, transférer les données
// vers un bitmap pf8bit nommé 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;

  // Copie des Scalines pf4bit vers les Scanlines pf8bit
  FOR j := 0 TO Bitmap4.Height-1 DO
  BEGIN
    Row4 := Bitmap4.Scanline[j];  // Scanline origine
    Row8 := Bitmap8.Scanline[j];  // Scanline destination

    // Width[Bytes] = Width[Pixels] / 2 pour un Bitmap pf4bit
    // On suppose que la largeur est paire.
    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

Voici un autre exemple : comment utiliser GetPaletteEntries et convertir un Bitmap pf8bit vers un Bitmap pf24bit. Cet exemple est présenté seulement à titre de démonstration, car pour convertir un bitmap 8 bits en bit map 24 bits, il suffit d'affecter une nouvelle valeur à la propriété PixelFormat.

Listing 5 : copier les données d'un bitmap pf4bit dans un bitmap pf8bit
Sélectionnez

// efg, 24 August 1998

unit ScreenGetPaletteEntries;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    ButtonReadpf8bit: TButton;
    Memo1: TMemo;
    Image2: TImage;
    procedure ButtonReadpf8bitClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
{$R *.DFM}

CONST
  MaxPixelCount = 65536;

TYPE
  TRGBArray = ARRAY[0..MaxPixelCount-1] OF TRGBTriple;
  pRGBArray = ^TRGBArray;

procedure TForm1.ButtonReadpf8bitClick(Sender: TObject);
  VAR
    i                :  CARDINAL;
    index            :  BYTE;
    j                :  CARDINAL;
    LogicalPalette   :  TMaxLogPalette;
    PaletteEntryCount:  CARDINAL;
    Bitmap8          :  TBitmap;
    Bitmap24         :  TBitmap;
    Row8             :  pByteArray;
    Row24            :  pRGBArray;
begin
  Bitmap8 := TBitmap.Create;
  TRY
    Bitmap8.LoadFromFile('Deer.BMP');

    // Affichage du Bitmap pf8bit
    Image1.Picture.Graphic := Bitmap8;

    IF   Bitmap8.PixelFormat <> pf8bit
    THEN ShowMessage('Format attendu : 8-bits/pixel BMP');

    IF   Bitmap8.Palette = 0
    THEN ShowMessage ('Pas de palette avec le BMP')
    ELSE BEGIN
      PaletteEntryCount := GetPaletteEntries(Bitmap8.Palette, 0, 255,
                                             LogicalPalette.palPalEntry);
      FOR i := 0 TO PaletteEntryCount-1 DO
      BEGIN
         Memo1.Lines.Add(Format('%3.3d %3.3d %3.3d %3.3d %d',
           [i, LogicalPalette.palPalEntry[i].peRed,
               LogicalPalette.palPalEntry[i].peGreen,
               LogicalPalette.palPalEntry[i].peBlue,
               LogicalPalette.palPalEntry[i].peFlags]))
      END;

      Bitmap24 := TBitmap.Create;
      TRY
        Bitmap24.Width  := Bitmap8.Width;
        Bitmap24.Height := Bitmap8.Height;
        Bitmap24.PixelFormat := pf24bit;

        FOR j := 0 TO Bitmap8.Height-1 DO
        BEGIN
          Row8  := Bitmap8.Scanline[j];
          Row24 := Bitmap24.Scanline[j];

          FOR i := 0 TO Bitmap8.Width-1 DO
          BEGIN
            index := Row8[i];  // index dans la palette
            WITH Row24[i] DO
            BEGIN
              rgbtRed   := LogicalPalette.palPalEntry[index].peRed;
              rgbtGreen := LogicalPalette.palPalEntry[index].peGreen;
              rgbtBlue  := LogicalPalette.palPalEntry[index].peBlue
            END
          END

        END;

        // Sauvegarde dans la nouveau BMP
        Bitmap24.SaveToFile('Deer24.BMP');

        // Affichage du BMP 24-bit (Sans palette maintenant)
        Image2.Picture.Graphic := Bitmap24;
      FINALLY
        Bitmap24.Free
      END

    END
  FINALLY
    Bitmap8.Free
  END
end;

end.

Bitmaps pf8bit en mémoire.
Vous pouvez créer un bitmap pf8bit en mémoire, avec sa palette, mais l'apparence de ce bitmap peut dépendre du mode vidéo actif. Si vous utilisez un mode couleurs 15/16 bits ou couleurs vrais (24/32 bits ) il est possible de voir toutes les couleurs de la palette 256 couleurs d'un bitmap pf8bit. ( voir image suivante )

Image non disponible

Cependant, l'affichage du même bitmap dans un mode 256 couleurs, va perdre 20 des 256 couleurs de la palette comme le montre l'image suivante.

Image non disponible

Windows prend 20 des 256 couleurs pour l'affichage des composants en mode 256 couleurs. Le reste des 236 couleurs peut être utilisé par l'application.

Image non disponible

Cet exemple de palette 8bits peut être téléchargé ici pour vos propres essais ( Delphi 3, 4 et 5 ). Faites attention, lorsque vous créez des bitmaps pf8bit avec leur palette, que leur affichage sera dans un mode vidéo "couleurs vraies".

Voir UseNet Post sur la méthode de création d'un bitmap avec un dégradé de 256 niveaux de gris.

Voir le mail de Dean Verhoeven sur l'utilisation de SetDIBColorTable pour modifier la palette d'un bitmap pf8bit.

Pour avoir des informations complémentaires sur les palettes, voir Color, Section B de la page Delphi Graphics Algorithms.

I-F. Format pf15bit et pf16bit

Tous les formats de pixel ne sont sont pas forcément disponibles sur tous les PC. Par exemple pf15bit n'est pas disponible sur certaines machines à cause de limitations de la carte vidéo ou de son driver. Voici une méthode pour vérifier que le format pf15bit est supporté :

 
Sélectionnez

BEGIN
  bitmap := TBitmap.Create;
  TRY
    bitmap.Width  := 32;
    bitmap.Height := 32;
    bitmap.PixelFormat := pf15Bit; 
    IF   bitmap.PixelFormat <> pf15bit
    THEN ShowMessage('pf15bit non supporté !');
...

En cas de doute il faut vérifier que le format de pixel créé est correct. Apparemment PixelFormat prend la valeur pfCustom si le format demandé n'est pas disponible. Voir le UseNet Post de Robert Rossmair sur la possibilité de contourner le problème. Et aussi le UseNet Post de Colin Wilson sur les cas de pf15bit/pf16bit aboutissant à pfCustom.

Utilisez le type pWordArray défini dans SysUtils.pas pour accéder aux pixels d'un bitmap pf15bit ou pf16bit. L'utilisation de ce type limite la taille de l'image à 16384x16384 mais ce n'est pas vraiment une limitation, car la création d'un tel bitmap va prendre toutes les ressources sytèmes, particulièrement sous Windows 95/98.

La disposition des bits dans un mot, pour chaque pixel, est le suivant :

 
Sélectionnez

pf15bit:  0 rrrrr vvvvv bbbbb
pf16bit:  rrrrr vvvvvv bbbbb

Un pixel pf15bit a 5 bits pour chaque couleur R, V et B. Par contre, un pixel pf16bit possède un bit de plus pour la couleur verte. Le vert a un bit de plus car l'oeil est plus sensible au vert qu'au bleu et rouge.

Le listing 6 montre comment créer un bitmap pf15bit rempli avec des pixels jaunes. Notez qu'un pixel avec rouge=31, vert=31 et bleu=0 est jaune.

Listing 6 : Création d'un bitmap pf15bit jaune
Sélectionnez

VAR
  Bitmap: TBitmap;
  i     : INTEGER;
  j     : INTEGER;

  R     : 0..31; // chaque composante RVB a 5 bits
  G     : 0..31;
  B     : 0..31;

  RGB   : WORD;
  Row   : pWordArray; // de 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" utilisant ScanLine
  // Les bits dans chaque pixels sont diposés selon :
  // 0rrrrrvvvvvbbbbb. Le bit de poids fort est ignoré
  RGB := (R SHL 10) OR (G SHL 5) OR B; // "Jaune"

  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

Voici un exemple similaire pour un bitmap pf16bit mais avec R=31 et V=63.

 
Sélectionnez

procedure TForm1.ButtonFillYellowPf16bitClick(Sender: TObject);

  VAR
    Bitmap:  TBitmap;
    i     :  INTEGER;
    j     :  INTEGER;

    R     :  0..31;  // 5 bits
    G     :  0..63;  // 6 bits (l'oeil est plus sensible au vert)
    B     :  0..31;  // 5 bits

    RGB   :  WORD;
    Row   :  pWordArray;   // de SysUtils
begin
  Bitmap := TBitmap.Create;
  TRY
    Bitmap.Width  := Image1.Width;
    Bitmap.Height := Image1.Height;
    Bitmap.PixelFormat := pf16bit;

    R := 31;    // Max rouge
    G := 63;    // Max vert
    B :=  0;    // Pas de bleu

    // "FillRect" en utilisant ScanLine
    RGB := (R SHL 11) OR (G SHL 5) OR B;  // "Jaune"

    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
end;

Lors de la conversion d'un bitmap pt15bit ( 5 bits par couleur ) vers un bitmap pf24bit ( 8 bits par couleur ) que se passe-t-il avec les trois bits supplémentaires ? Voir le UseNet Post de efg à ce sujet. La conversion n'est pas déterminable. Le résultat dépend de la carte vidéo.

Suivant le UseNet Post de Ian Martin, Photoshop ne lit pas correctement les bitmaps pf16bits générés par Delphi. La solution de Ian est d'utiliser plutôt des bitmaps pf24bits.

Voir aussi le UseNet Post de Paul Nicholls sur l'utilisation de BASM pour modifier un bitmap pf16bit.

I-G. Format pf24bit

Pour les bitmaps pf24Bit je définis ( j'espère que Borland le ferait aussi ) le type suivant dans le listing 7. Il est similaire au type TByteArray.

Listing 7 : Définition de TRGBTripleArray
Sélectionnez

CONST
  PixelCountMax = 32768;

TYPE
  pRGBTripleArray = ^TRGBTripleArray;
  TRGBTripleArray = ARRAY[0..PixelCountMax-1] OF TRGBTriple;

L'utilisation d'une valeur importante pour PixelCountMax a deux avantages. Aucun bitmap ne peut être créé de cette taille pour le moment et ceci permet de conserver le contrôle d'étendue sur la propriété ScanLine.

Comme la définition du listing 7 met des limites pour l'étendue de l'index ( vous pouvez réduire cette étendue ), la définition suivante est sans doute plus simple :

 
Sélectionnez

TYPE   TRGBTripleArray = ARRAY[WORD] OF TRGBTriple;

Borland définit un TRBGTripleArray dans l'unité Graphics.pas mais c'est un ARRAY[BYTE] of TRGBTriple.

Commentaire de Danny Thorpe (Borland R&D) :
Ceci est fait pour l'utilisation dans le traitement des palettes de couleurs. Ce n'est pas fait pour accéder aux pixels. Vous remarquerez que dans les évolutions de D1 à D4, j'ai éliminé beaucoup de réservations de mémoire tampon dans les routines des bitmaps. L'un des plus grands gains de temps était de ne pas allouer de la mémoire temporaire pour les palettes de couleurs. A la place juste une variable locale sur la pile de la taille maximum possible dont j'avais besoin : 256 RGBTriples. Ceci prend entre 700 et 1000 octets dans la pile mais c'est négligeable sous Win32, et l'espace est récupéré en quelques nanosecondes.
Ceci ne fonctionnerait pas avec un TRGBTriple sur une étendue WORD. Même si cela tient dans la pile, ceci resterait trop consommateur.

L'utilisation du type TRGBTripleArray de Borland poserait des problèmes même pour un bitmap d'une taille "normale", car l'étendue doit être entre 0 et 255. La définition que je propose n'a pas cette limitation.

La définition de Borland dans Windows.pas de TRGBTriple est la suivante :

 
Sélectionnez

pRGBTriple = ^TRGBTriple;
TRGBTriple = 
PACKED RECORD
  rgbtBlue : BYTE;
  rgbtGreen: BYTE;
  rgbtRed  : BYTE;
END;

Le listing 8 montre comment créer un bitmap pf24bit dont les pixels sont jaunes. Notez que les pixels avec rouge=255, vert=255 et bleu=0 sont jaunes.

Listing 8A : Création d'un bitmap pf24bit jaune
Sélectionnez

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; // pixels jaunes
        rgbtGreen := 255;
        rgbtBlue  :=   0;
      END
    END
  END;

  // Affichage à l'écran
  ImageRGB.Picture.Graphic := Bitmap;

FINALLY
  Bitmap.Free
END

à partir de Delphi 4.02 ou supérieur, on a une approche simplifiée en utilisant une constante TRGBTriple :

Linsting 8B : avec l'utilisation de CONST TRGBTriple
Sélectionnez

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;

  // Affichage à l'écran
  ImageRGB.Picture.Graphic := Bitmap;

FINALLY
  Bitmap.Free
END

Mais comment un TColor peut-il être converti en TRGBTriple ?

 
Sélectionnez

FUNCTION  ColorToRGBTriple(CONST Color:  TColor):  TRGBTriple;
BEGIN
  WITH RESULT DO
  BEGIN
    rgbtRed   := GetRValue(Color);
    rgbtGreen := GetGValue(Color);
    rgbtBlue  := GetBValue(Color)
  END
END {ColorToRGBTriple};  

Faites attention que TColor peut avoir différents formats internes. Le code ci-dessus suppose que le format est $00BBVVRR qui peut être créé en utilisant les fonctions RGB.

Comment un bitmap pf24bit peut-il être converti en un bitmap pf15bit ?
Considérons la méthode "brute" avec ScanLine et la méthode "simple" avec PixelFormat. Voir le listing 9.

Listing 9 : Convertir un bitmap pf24bit en bitmap pf15bit
Sélectionnez

VAR
  Bitmap15:  TBitmap;
  Bitmap  :  TBitmap;
  i       :  INTEGER;
  j       :  INTEGER;
  Row15   :  pWordArray;
  Row24   :  pRGBTripleArray;
...
Bitmap := TBitmap.Create;
TRY
  Bitmap.LoadFromFile('N:\Images\Flowers\Tulip3.BMP');

  // Conversion en utilisant la méthode "brute"
  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;

  // Conversion en utilisant la méthode "simple"
  ASSERT(Bitmap.PixelFormat = pf24bit); // verifie pf24bit
  Bitmap.PixelFormat := pf15bit; // L'affection effectue la conversion
  Bitmap.SaveToFile('Tulip3-15B.BMP')
FINALLY
  Bitmap.Free
END

// Tulip3-15A.BMP et Tulip3-15B.BMP doivent être identiques.

Dans l'exemple ci-dessus, les deux bitmaps sont identiques en comparant pixel à pixel. Par contre, ils peuvent avoir un CRC32 différent, les fichiers ne seront donc pas identiques. Sans doute à cause d'un en-tête différent entre les deux fichiers.

Une meilleure approche dans la réduction de 24-bits par pixel à 15/16 bits par pixel est donnée par Phil McRevis dans un UseNet post ou il suggère d'utiliser la méthode d'arrondi Floyd-Steinberg.

Autre exemple : Comment créer un bitmap à partir de données numériques ?

Image non disponible

Voir aussi le UseNet post d'efg sur la méthode pour retourner un bitmap en écrivant les lignes dans un TMemoryStream puis en lisant les lignes dans un second bitmap dans l'ordre inverse.

I-H. Format pf32bits

Analogue au TRBGTriple, voici la définition d'un TRGBQuadArray pour travailler avec les bitmaps pf32bit. Voir listing 10.

Définition de TRGBQuadArray
Sélectionnez

CONST
  PixelCountMax = 32768;

TYPE
  pRGBQuadArray = ^TRGBQuadArray;
  TRGBQuadArray = ARRAY[0..PixelCountMax-1] OF TRGBQuad;

Une définition simplifiée, sans utiliser PixelCountMax, serait :

 
Sélectionnez

TYPE   TRGBQuadArray = ARRAY[WORD] OF TRGBQuad; 

La définition de Borland, dans Windows.pas du TRGBQuad est la suivante :

 
Sélectionnez

pRGBQuad = ^TRGBQuad;
TRGBQuad = 
PACKED RECORD
  rgbBlue : BYTE;
  rgbGreen: BYTE;
  rgbRed  : BYTE;
  rgbReserved:  BYTE
END;

Tant que l'on parle des informations de couleur, notez que TRGBQuad est équivalent à TRGBTriple. Les deux types possèdent 24 bits pour le codage de la couleur : 8 pour le rouge, 8 pour le vert et 8 pour le bleu. Le TRGBQuad possède un octet réservé supplémentaire qui est parfois appelé octet "alpha" ( surtout dans le monde du Mac ). Cet octet est utilisé par certaines applications pour stocker un masque de niveaux de gris. Mais il n'y a aucune information fiable sur le contenu de cet octet "alpha". Ce contenu n'est pas prévisible. Voir le UseNet post de Stan ou "Vous ne pouvez jamais connaître le contenu de l'octet alpha".

Commentaire de Danny Thorpe (Borland R&D) :
NT supporte à peu près n'importe quelle disposition des bits RGB. La seule limitation est que les bits de chaque couleur doivent être contigus. Il peut être facile de lire un fichier RVB de type big-indian ( comme les fichiers RAW de Sun ou autre ) simplement en créant le DIBsection avec les masques BVR. Juste pour rire, j'ai créé une fois un DIBSection de 32bpp avec une information de couleur sur 24 bits, puis j'ai créé un autre DIBSection en utilisant le même tampon pour les pixels, mais en plaçant les données dans l'octet "alpha" réservé pour les données des points 32bpp.

Une utilisation de ScanLine d'un bitmap pf32bit est de créer un tableau dynamique de valeur Single issues d'un calcul numérique. Ce genre de tableau peut être sauvé sur le disque en utlisant la méthode SaveToFile et lu en utilisant la méthode LoadFromFile. Ces nombres peuvent être "affichés" dans un TImage ou chaque pixel représente une fraction. L'octet réservé du TRGBQuad correspond à la partie exposant du type Single et n'influera pas sur l'image. Les images ainsi créées, à partir de données scientifiques, peuvent montrer des dessins très intéressants. Voir l'exemple d'utilisation d'un bitmap pf32bit pour contenir et afficher des valeurs Single IEEE issues de Lyapunov Exponents Lab Report le Fractals Show 2 Lab Report montre comment utiliser un bitmap pf32bit comme une matrice d'entiers de quatre octets.

Dans certains tests l'utilisation d'un bitmap pf32bit est environ 5% plus rapide qu'un bitmap pf24bit. Sans doute à cause de l'alignement des points sur une limite de 32 bits. Donc l'utilisation de 24 bits par pixel augmente légèrement le temps de calcul, mais pas suffisamment en comparaison de l'espace récupéré entre le format pf32bit et pf24bit.

Voir aussi :
-l'exemple de Ken Florentino sur l'utilisation de bitmaps pf32bit en assembleur.
-Le UseNet post d'efg sur "Enigme pf32bit : 'alpha byte' indéterminé dans un nouveau bitmap ?".
-Kylix Nota : Le UseNet post pour un exemple de pf32bit avec Kylix.
-Le UseNet post d'efg sur "Comment transtyper un TColor décalé en TRGBQuad ?"

 
Sélectionnez

VAR
  Color :  TColor;
  yellow:  TRGBQuad;
..
  Color := clYellow;
  yellow := TRGBQuad(Color SHL 8);

En partant d'un TColor avec un format interne de $00BBVVRR, si vous le décalez de 8 bits vers la gauche, vous obtenez $BBVVRR00, ce qui correspond au format interne d'un TRGBQuad. Un transtypage permet ensuite l'affectation du décalage à un TRGBQuad.

I-I. Conversion de format

Affecter une nouvelle valeur à PixelFormat pour effectuer une conversion d'un format de pixel à un autre. Ceci fonctionne bien pour la conversion d'un format de pixel bas vers un format de pixel plus important ou pour la conversion de PixelFormat n'ayant pas à traiter des palettes ( ex : pf15bit vers/depuis pf24bit ). Mais affecter un nouveau PixelFormat ne garantit pas que la palette obtenue soit correcte si elle est utile.

Un bitmap pf24bit peut avoir des milliers de couleurs. Par exemple le Mandrill monkey bitmap est un tableau 512x512=262144 pixels, mais a 230427 couleurs différentes ! ( L'utilitaire Show Image donne le nombre de couleurs différentes d'une image dans le coin en bas à gauche de l'écran ). En mode 256 couleurs ( pf8bit ), Windows réserve normalement 20 couleurs pour l'affichage des boutons, panneaux, icônes, etc... , laissant seulement 236 couleurs pour l'application. Pour afficher l'image "Mandrill monkey" un algorithme est nécessaire pour déterminer les 236 meilleures couleurs des 230427 de l'image.

Regardez la démo Show Demo One Lab Report pour voir un algorithme qui crée une palette pour afficher une image de 24 bits par pixel dans un mode 256 couleurs. Cet exemple prend chaque TRGBTriple dans chaque ScanLine et recherche la couleur de la palette la plus approchante, il n'effectue pas une conversion d'un bitmap pf24bit en bitmap pf8bit.

Vous êtes très chanceux si vos images pf24bit s'affichent correctement en mode 256 couleurs.

II. Autres notes sur ScanLine

Question( UseNet post de Paul Nicholls ) : "Si je veux sauver des pointeurs issus de ScanLine d'un bitmap dans des variables, quand dois-je mettre à jour ces variables pour que les pointeurs soient de nouveau valides ?"
Réponse( UseNet post de Steve Schafer ) : "Je ne pense pas qu'il soit conseillé de garder une copie des pointeurs ScanLine. Même si ça peut être fait en sécurité dans certaines circonstances et sur une version de Delphi, il peut être dangereux de le faire dans les mêmes circonstances dans le futur."
"Obtenez un pointeur ScanLine, utilisez-le et jetez-le !"

UseNet post de Peter Haas sur l'utilisation de Bitmap.Dormant après l'affectation de PixelFormat pour corriger un problème dans Delphi 1 à 4 avec le TBitmapInfoHeader.

UseNet post de Finn Tolderlund sur ScanLine et FreeImage.

Exemples de Eric Sibert sur les bitmaps pf24bit et pf32bit lors de la migration vers Kylix.
http://esibert.developpez.com/kylix/migration/image/

UseNet post de Matthijs Laan sur l'utlilisation de MMX pour effectuer un XOR sur un bitmap.

UseNet post de Andrew Rybenkov sur l'utilisation de GetDIBits/SetDIBits comme alternative à ScanLine.

III. Exemples utilisant ScanLine

III-A. Le programme Daisy

Le programme Daisy utilise la même technique que celle du listing 8 pour créer une image pf24bit. Le détail de la méthode DrawDaisy montre comment cette image est créée. Brièvement, les plans rouge et vert de cette image contiennent toutes les nuances de ces couleurs. Le plan bleu contient le "Daisy" avec seulement la plus lumineuse des valeurs bleues. Pour cette application, vous devez disposer au minimum d'un écran 800x600 et d'un mode vidéo de 15 bits par couleur ou plus.

Afficher une image 24 bits par couleur est facile avec un mode vidéo de 15 bits par couleur ou supérieur car windows n'utilise de palettes que pour les modes 256 couleurs ou inférieurs. Si vous essayez d'afficher un bitmap pf24bit dans un mode de seulement 256 couleurs, vous êtes à la merci de la palette courante de Windows. Voir le Show Demo One Lab Report pour une alternative à ce problème.

III-B. Le programme Split

Le programme Split est un simple traitement d'image pout étudier les plans de couleurs d'une image. Le programme SPLIT lit le fichier DAISY.BMP créé par le programme DAISY ( ou un autre fichier BMP de 24 bits par couleur ). L'appui sur les boutons à gauche permet d'afficher les plans rouge, vert et bleu correspondants.

Le programme SPLIT sauvegarde l'image originale dans BitmapRGB en tant que base pour la création des autres bitmaps.

Quand la case à cocher "Monochrome" située près du bouton "RGB Composite" est cochée, chaque composante rouge, vert et bleu est affectée à la même valeur. Cette valeur est définie par (T + V + B) / 3. Le bitmap obtenu ainsi, BitmapGray, est assigné à Image.Picture.Graphic pour affichage. Voir listing 11.

MakeShadesofGrayImage à partir d'une image RVB
Sélectionnez

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
        // Intensité = (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;

Une autre "meilleure" méthode pour créer des niveaux de gris peut être utilisée. Voir le Spectra lab report pour deux autres méthodes basées sur "Y", le niveau de gris utilisé pour la conversion des informations de couleur ( YUV/YIQ ) dans l'affichage sur une télévision noir et blanc.

Pour afficher le plan rouge, les valeurs rouges des pixels ( les valeurs rgbtRed ) sont assignées à un autre bitmap, nommé BitmapR. Les valeurs bleues et vertes des pixels sont quant à elles mises à zéro. Quand la case "Monochrome" est cochée, le niveau de rouge est affecté à rgbtRed, rgbtBlue et rgbtGreen ; en résulte une image en niveaux de gris.

L'explication de la conversion de RVB ( Rouge-Vert-Bleu ) en HSV ( Hue-Saturation-Valeur ) est décrite dans le HSV Lab Report. Un excellent livre sur les conversions de couleurs ( et tout ce qui concerne l'imagerie sur PC ) : Computer Graphics Principles and Practice de Foley, et al, Addison-Wesley, 1996. Ou bien regardez dans l'article général Color Information dans la Reference Library par efg. Pour les conventions de couleur dans Delphi regardez Color, Section B de la page Delphi Graphics Algorithms.

III-C. Rotation d'une image

Comme présentée précédemment, la rotation d'un bitmap est trop lente avec la propriété Pixels. L'utilisation de ScanLine pour la rotation de n'importe quel angle est plus rapide. Le Rotate Scanline Lab Report montre que la rotation d'un bitmap pf24bit de 640x480 par degré dans le sens des aiguilles d'une montre prend quand même plus d'une seconde sur un Pentium 166 MHz.

Chaque pixel n'est pas tourné dans sa nouvelle position. Vous partez de l'image tournée et considerez où le pixel est dans l'image originelle. En effectuant la rotation inverse, le pixel le plus proche de l'image originale est choisi. A cause de l'utilisation d'entiers dans les calculs, certains artifices peuvent être introduits dans la rotation. Habituellement l'anti-aliasing n'est pas nécessaire dans la rotation d'images pf24bit de la plupart des objets. L'anti-aliasing doit être utilisé pour la rotation d'images précises, et surtout dans le cas de textes.

Voir aussi le Flip/Reverse/Rotate Lab Report.

III-D. Permutation de couleurs dans un bitmap pf24bit

L'ancienne limitation de la palette VGA ne peut fonctionner avec des images 24 bits. Souvenez-vous : Windows utilise des palettes seulement avec les modes 256 couleurs ou inférieurs. Les palettes ne sont pas utilisées dans les modes couleurs ( 15 ou 16 bits ) et couleurs vraies ( 24 bits ou plus ).

Quand Windows utilise des palettes, vous avez seulement 236 couleurs disponibles, car les 10 premières et 10 dernières couleurs de la palette sont définies par Windows. Même avec cette technique, la permutation de couleurs d'un bitmap pf24bit est un peu lente sans accélération matérielle. Le Color Cycle Lab Report utilise une table de correspondance de 1280 couleurs ( 5*256 ), qui est un nombre bien plus grand qu'une palette windows normale.

La méthode FormCreate définit les entrées dans le ARRAY of TRGBTriples : ColorCycle. Les 256 premières couleurs sont un dégradé de rouge, suvi par 256 niveaux de vert et 256 niveaux de bleu. Le quatrième ensemble est très similaire à la palette "Tempête de feu" utilisée dans le programme "FractInt fractal". Le cinquième ensemble définit des niveaux de gris.

Après avoir lancé le programme CycleColor, l'image fractale est créée en un temps de 90 secondes sur un pentium 166 MHz. ( Les maths utilisées pour la création de cette image sortent du cadre de cet article. ) Une fois que l'image est terminée, la case "Cycle Colors" est activée. L'image fractale est un bitmap pf8bit ( créé suivant une méthode approchant celle du listing 4 ).

Quand la case "Cycle Colors" est cochée, l'événement OnIdle de l'application prend le bitmap pf8bit nommé BitmapBase et définit tous les pixels RVB de BitmapRGB en utilisant le tableau ColorCycle. Voir le listing 12 ci-dessous. Le fait de décocher la case stoppe la permutation des couleurs. Légèrement lente, la permutation des couleurs effectuée complètement par programme est quand même impressionnante.

Permutation de couleurs en utilisant la tâche Idle
Sélectionnez

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};

III-E. Autres exemples

IV. Utilisation inappropriée de ScanLine

Par Danny Thorpe(Borland R&D)

Beaucoup de gens se précipitent pour utiliser ScanLine pour des opérations qui pourraient être effectuées plus rapidement par le GDI. L'avantage de ScanLines[] est de donner un accès direct aux pixels avec un minimum de temps de traitement. Ceci ne signifie pas que ScanLines[] est le moyen le plus rapide pour manipuler le contenu d'un bitmap. Beaucoup de cartes vidéo aujourd'hui contiennent des moteurs dédiés intégrés très sophistiqués, qui peuvent effectuer des opérations sur les données d'un bitmap beaucoup plus rapidement que ne le fait le CPU. J'ai vu des cartes vidéos remettre à zéro un bitmap entier avant même que le CPU n'ai fini la première ligne de ScanLines. Le CPU ne peut faire qu'un seul point à la fois, et chaque accès est un accès à la mémoire pouvant mettre le pipeline CPU en attente de réponse du cache. Si la partie DIBSection réside en mémoire vidéo le CPU doit passer à travers le bus PCI ou AGP pour la lire. La carte vidéo n'a aucune de ces restrictions, elle a un accès immédiat et direct sans temps de latence à la mémoire vidéo. Et beaucoup de moteurs intégrés sont conçus pour traiter plusieurs pixels en une seule fois. Remplir la mémoire vidéo avec des zéros ou une couleur ( ou un pinceau : Brush ) est aussi une opération prioritaire pour le hardware de la carte vidéo. J'ai entendu dire que le secret d'un carte 3D de haut niveau pour remplir de zéro extrêmement vite, n'est pas de mettre des zéros dans la RAM, mais de couper l'alimentation d'une partie de la mémoire !

Un exemple : le listing 3 pour inverser un bitmap en balayant les pixels et en effectuant un NOT sur chacun d'entre eux. Je doute sérieusement que cela ait les mêmes performances que PatBlt(Bitmap.Canvas.Handle, 0, 0, Bitmap.Width, Bitmap.Height, DSTINVERT), même sur un vieux tampon VGA tramé. Le listing 3 est bien sûr donné à titre démonstratif, comme le fait que la documentation de Delphi mentionne Canvas.Pixels[], les gens vont adopter le code exemple comme la vérité, sans se soucier à quel point c'est inapproprié pour le travail productif.

Même observation pour le listing 6 : la fonction est décrite comme équivalente à FillRect avec un pinceau jaune. Il n'est pas précisé que FillRect sera sans doute beaucoup plus rapide. Cet exemple n'est donc que démonstratif et non productif.

Pareillement, mixer des palettes de bitmap peut être effectué sans manipuler les pixels. L'astuce est de fusionner les palettes en premier, d'assigner la nouvelle palette au bitmap de destination et enfin d'effectuer un blit du bitmap source sur celui de destination. Le GDI et le hardware de la carte vidéo vont faire la correspondance de couleurs pour vous.

Aussi, depuis Delphi 4, assigner un Handle de palette à un bitmap va recalculer les pixels du bitmap pour obtenir la couleur la plus approchante dans la nouvelle palette. Delphi 3 et précédents ne le font pas.

V. Optimisation

V-A. Minimiser le nombre d'accès à ScanLines

Plusieurs fois dans les newsgroups Delphi quelqu'un suggère que le nombre d'accès à ScanLines peut être minimisé par une technique sans le temps de traitement qui doit exister à chaque appel. Par exemple, plutôt que d'accéder à ScanLine une fois par rangée, ( comme beaucoup d'exemples ici ) avec un peu de code supplémentaire ScanLine peut n'être accédé que seulement deux fois au début de la boucle de traitement des lignes. Un pointeur peut être incrémenté en utilisation les opérations arithmétiques entières plutôt que d'appeler ScanLine sur chaque rangée. Voir le Listing 13 pour un exemple.

Listing 13 : Minimiser les accès à ScanLines[]
Sélectionnez

procedure TForm1.ButtonOptimizedClick(Sender: TObject);
  VAR
    Bitmap       :  TBitmap;
    Delta        :  INTEGER;
    i            :  INTEGER;
    j            :  INTEGER;
    k            :  INTEGER;
    LoopCount    :  INTEGER;
    row          :  pRGBTripleArray;
    ScanlineBytes:  INTEGER;
    StartTime    :  DWORD; // DWORD pour que D3 et D4 soient contents
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;

Mais quelle est l'efficacité de cette technique ? Pour le déterminer un petit programme ScanlineTiming D3/D4 a été écrit pour comparer la technique "brute" accédant à ScanLine pour chaque rangée à la technique expliquée ci-dessus, dans le listing 13. Les résultats sont affichés dans le tableau suivant :

Temps ( ± écart type ) pour un bitmap de 640x480 basé sur 5 essais de 100 bitmaps chacun.

Vitesse CPU MHz "Brute" ms/bitmap "optimisée" ms/bitmap gain ms/bitmap
120 88.30 ± 0.60 187.90 ± 0.90 0.40
166 70.90 ± 0.07 69.80 ± 0.04 1.10
400 16.27 ± 0.02 15.40 ± 0.00 0.87
450 17.69 ± 0.35 16.78 ± 0.32 0.91

A mon avis, cette technique est certainement la dernière chose à penser quand vous travaillez avec un bitmap. Tous les autres algorithmes doivent être affinés avant. Rarement sauver une milliseconde par bitmap vaut l'optimisation, peut-être n'est-ce significatif qu'avec des applications temps-réel. Sur les PC les plus rapides, cela ne représente qu'un gain de 5%.

Commentaire par mail de Robert Lee à propos de cette optimisation :
... L'efficacité de cette technique dépend surtout de ce que vous faites avec le bitmap. Par exemple, la moitié du temps dans votre exemple est passé à créer et détruire le bitmap lui-même. Si le bitmap en question est déjà créé, alors l'effet relatif de cette technique va augmenter de 10%. Aussi, si les dimensions sont changées en quelque chose de long et étroit, ou si l'opération porte sur deux bitmaps ou plus ( mélange... ) alors l'effet sera progressivement plus significatif. Juste avec un petit essai, j'ai doublé la différence seulement en changeant la taille du bitmap.
Ma stratégie préférée pour mesurer l'impact d'une technique d'optimisation est de faire état du "meilleur" cas et du cas "typique". Alors la cas typique est sans doute de 5% ( voire même moins ), mais le "meilleur" effet peut être de 2 à 4x. [ merci, Robert, pour l'éclaircissement ]

Commentaire de Danny Thorpe (Borland R&D)
Au sujet de l'impact sur les performances d'un appel répété à ScanLines : oui, il y a un gain potentiel. Le tampon de la DIBSection en mémoire n'est pas garanti comme étant cohérent ( à cause d'opérations GDI récentes ) tant que GDIFlush n'est pas appelé avant d'accéder au pointeur. La propriété ScanLine[] doit appeler GDIFlush pour être sûre que le tampon est synchronisé. Si le bitmap a été modifié par des appels GDI, alors l'appel de GDIFlush peut bloquer en attendant que la file du GDI soit vide. Si aucune modification n'est en cours, GDIFlush devrait finir immédiatement, mais il y a toujours un surplus de temps dû à un appel supplémentaire.
De plus, TBitmap.GetScanLines() appelle TBitMap.Changing, qui peut avoir à créer un bitmap complètement nouveau avec une copie de l'image si celui-ci est partagé. Ceci va complètement effondrer les performances dès le premier appel à ScanLines, et perdre un peu même si celui ci n'est pas partagé.
La technique montrée dans le listing 13 pour minimiser le temps perdu par ScanLines[] a été développée par moi lors de l'écriture de la classe TJPEGImage. Il y a plus de travail que ce que l'on peut voir et qui devrait être mentionné avec le listing. Le moyen le plus facile pour éliminer les appels à ScanLines[] est de faire son propre pointeur et utiliser les opérations arithmétiques pour le faire avancer d'un début de ligne à la suivante. Il y a deux risques à le faire :
1) Tenir compte de l'alignement DWord de chaque ligne
2) La disposition physique des lignes dans le tampon mémoire : les DIBs peuvent être orientés de "haut en bas", où la première ligne de pixels réside dans les premiers octets de la zone tampon ; ou de "bas en haut", où la première ligne de pixels réside dans les derniers octets de la zone tampon. Par expérience, de "bas en haut" est l'orientation la plus courante pour les DIBs, peut-être à cause de l'origine OS2 des BMPs.
La technique de soustraire l'adresse de ScanLine[0] à l'adresse de ScanLine[1] résout les deux problèmes très joliement. Cela donne la distance entre deux lignes ScanLines y compris l'ajustement pour l'alignement DWord si besoin. Et le signe de l'écart indique implicitement l'orientation du DIB en mémoire. Ceci élimine le besoin d'ajouter de coûteux tests conditionnels pour avancer dans la boucle. Il suffit d'incrémenter le pointeur de l'écart et il va se placer sur la ligne suivante, qu'elle soit avant ou après l'adresse en cours.

Envoyez-moi SVP si vous pensez que j'ai oublié quelque chose dans les tests ou si vous obtenez des différences significatives.

V-B. Accéder à ScanLine en utilisant l'assembleur

Voir le UseNet post de Paul Nicholls sur l'accès à un bitmap pf16bit en utilisant BASM.

Voir le UseNet post de Robert Rossmair sur le calcul de la couleur moyenne d'un bitmap.

Voir l'exemple de Ken Florentino sur l'utilisation d'un bitmap pf32bit en assembleur.

V-C. Copie d'une zone de données vers ScanLines

Dans un message du groupe borland.public.delphi, Mikael Stalvik veut créer un tableau d'entiers ( 4 octets ) en mémoire et le copier dans un bitmap pf32bit. J'ai créé deux exemples pour Mikael, un "non-optimisé" et un autre "optimisé". Mais les performances entre les deux sont équivalentes.

La méthode "non-optimisée" est de remplir les lignes dans l'ordre croissant ( de haut en bas ).

Listing 14 : Méthode 'non-optimisée' pour copier un tableau d'entiers dans ScanLine[]
Sélectionnez

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    // Quelque chose pour faire une image "intéressante"
      END
    END;
    Image1.Picture.Graphic := Bitmap;
    ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
  FINALLY
    Bitmap.Free;
  END
end;

La méthode optimisée utilise une simple opération "Move" pour copier toutes les données dans ScanLine. ( N.B. : il y a des conditions d'alignement sur ScanLine, chaque ligne de ScanLine doit être un multiple de 8 octets. Ces conditions sont ignorées ici. On suppose que le bitmap a la bonne largeur pour qu'aucun alignement ne soit ajouté à la fin de chaque ligne ).

Mais quelle est l'adresse de chaque pixel pf32bit dans un TBitMap ? Insérons le test IF suivant dans le code au-dessus pour les trouver :

 
Sélectionnez

 // Affiche l'adresse des "coins" du 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 ));

Voici les résultats pour quelques points particuliers d'un bitmap pf32bit :

Ligne\Colonne 0 ... ... ... 639
0 $8374E600 ... $8374EFFC
1 $8374DC00 ... $8374E5FC
... ... ... ...
479 $83623000 ... $836239FC

Notez la plus grande et la plus petite des adresses. Le début des adresses du bitmap est le pixel 0 de la ligne 479, et la plus grande adresse est celle du pixel 639 de la ligne 0. Donc les adresses augmentent de la gauche vers la droite, mais du bas vers le haut.

Mais ceci n'est pas la seule disposition possible d'un TBitmap. Comme Alex Denissov le souligne dans les NewsGroup, quand la valeur de BitmapInfo.bmiHeader.biHeight est négative, le point (0,0) est le coin haut-gauche. Quand cette valeur est positve, c'est le coin bas-gauche. Normalement un TBitmap à la point (0,0) dans le coin haut-gauche.

Un moyen simple de savoir si les adresses de ScanLine[] vont en augmentant ou en diminuant est de calculer :

 
Sélectionnez

ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(Bitmap.Scanline[0]);

Combien y a-t-il d'octets dans le bitmap ? Calculer ($8374EFFC + 4) - $83623000 = $12C000 = 1,228,800 = 4*640*480, ce qui est bien la valeur attendue.

Pour utiliser "Move", il faut des pointeurs pour les adresses "de" et "vers". L'adresse "Vers" est l'adresse du pixel 0 de la dernière rangée, comme montré ci-dessous :

Listing 15 : Méthode 'optimisée' pour copier un tableau d'entier dans ScanLine[]
Sélectionnez

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;   // Quelque chose d'intéressant
  Start := GetTickCount;
  Bitmap := TBitmap.Create;
  TRY
    Bitmap.PixelFormat := pf32bit;
    Bitmap.Width  := 640;
    Bitmap.Height := 480;
    p := Bitmap.Scanline[Bitmap.Height-1];  // Adresse de départ de la dernière rangée
    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 a été utilisé dans les deux cas pour mesurer l'efficacité de la méthode. Je n'ai pas pu détecter de différence significative entre les deux méthodes sur un Pentium III 650MHz.

Deux remarques pour terminer :
1 - Les deux méthodes présentées ci-dessus ne sont pas identiques. Les deux bitmaps obtenus sont retournés de haut en bas. Ce qui a été ignoré ici.
2 - Les données brutes (i*j des coordonnées des points) utilisées pour le calcul du bitmap affichent un bitmap intéressant.

VI. Problèmes inattendus avec l'utilisation de ScanLine ( Delphi 3 et 4)

VI-A. Corruption de l'image originelle suite à un Assign

Utiliser ScanLine juste après un TBitmap.Assign va modifier le bitmap originel ( problème avec Delphi 3 et 4, corrigé avec Delphi 5 ). TBimap.Assign doit être une copie complète ( toutes les données copiées ) et non pas une copie partielle ( simple copie du pointeur ). Actuellement, à cause du compteur de référencement, l'assignation n'est qu'une copie partielle tant qu'aucune opération "significative" n'est effectuée provoquant cette fois-ci la création d'une copie indépendante du bitmap. Ceci peut être parfois très subtil à déterminer.

Ci-dessous, l'assignation d'un seul pixel change la copie partielle en copie complète. La nécessité de ceci est soit un bug soit un problème de conception.

 
Sélectionnez

BitmapAlign := TBitmap.Create;
TRY
  // Assign ne fait pas une copie indépendante directement
  // Ce problème ne concerne que Delphi 3 et Delphi 4
  BitmapAlign.Assign(Bitmaps[2]);

  // On force BitmapAlign à être totalement indépendant de Bitmaps[2]
  // Ceci est une rustine nécessaire si ScanLine est la seule méthode
  // d'accès aux pixels. Sans ceci le bitmap originel et le nouveau
  // bitmap travaillent sur les mêmes données de pixels !!
  BitmapAlign.Canvas.Pixels[0,0] := Bitmaps[2].Canvas.Pixels[0,0];

<Code n'accédant aux pixels que par ScanLine>

FINALLY
  BitmapAlign.Free
END

Voici un exemple montrant ce problème : ScreenBitmapAssignEnigma.PAS

La solution de Finn Tolderlund (après avoir parcouru la VCL... ) est d'utiliser la commande :

 
Sélectionnez

BitmapAlign.Assign(Bitmaps[2]);
BitmapAlign.FreeImage;
<Code n'accédant aux pixels que par ScanLine>

Le FreeImage a pour effet de convertir la copie partielle en copie complète du bitmap. Merci à Finn pour cette astuce.

VI-B. L'erreur d'assignation d'un pixel pf24bit ( rare bug de l'optimisation de Delphi )

Le problème apparait dans Delphi 3 et Delphi 4, mais semble réparé dans la mise à jour 2 de Delphi 4 ( Build 5.104). Merci à Marko Peric de m'avoir informé que cette mise à jour corrige le problème.

Le code suivant sur la copie d'un TRGBTriple d'une ligne de ScanLine à une autre provoque une violation d'accès si les optimisations de Delphi sont activées.

 
Sélectionnez

VAR
  BitmapA  : TBitmap;
  BitmapB  : TBitmap;
  i        : INTEGER;
  j        : INTEGER;
  RGBTriple: TRGBTriple;
  rowInB   : pRGBTripleArray;
  rowInA   : pRGBTripleArray;

// Définition d'un Bitmap 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];  // Violation d'accès
  END
END;

La méthode suivante est le "meilleur" contournement que j'ai trouvé ( sans désactiver les optimisations qui font trop de différences dans les temps d'exécution ).

 
Sélectionnez

VAR
  RGBTriple: TRGBTriple;  

// Coutournement du problème avec les meilleures performances
RGBTriple := RowInA[i];
RowInB[i] := RGBTriple

Voir les détails additionnels sur ce problème. Etrangement, Borland pense que ce n'est pas un bug. Ils m'ont dit : "Nous avons déterminé que votre rapport d'erreur n'est pas un bug, mais peut-être une erreur de compréhension sur le fonctionnement du produit."

VI-C. Erreur interne C1127 ( Delphi4 ) et C1141 ( Delphi 5 )

VI-D. Présence simultanée des sections DDB et DIB sur le même bitmap

C'est un problème avec une implémentation complexe autorisant les sections DDB et DIB à exister simultanément sur un seul Bitmap, comme décrit dans le UseNet post de Takuo Nakamura.

Conclusion

Cette note technique couvre bon nombre d'exemples sur l'utilisation des propriétés ScanLine et PixelFormat pour manipuler les pixels de différentes façons. Beaucoup de traitements d'images et de fonctions graphiques peuvent être écrites en Delphi avec des procédures très rapides utilisant ScanLine. J'espère que d'autres vont découvrir combien sont utiles les possibilités de développement RAD de Delphi dans le traitement d'images et l'imagerie sur PC.

Merci à Danny Thorpe, ingénieur confirmé de Delphi R&D pour ses remarques constructives.

Merci à Gus Elliot pour m'avoir signalé une omission dans le listing 13. ( 25 Jan 2001)

Merci à Alacazam pour la correction orthographique de la version traduite.

Références

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © Earl F. Glynn et copyright © 2003 Bruno Guérangé pour la version Française . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts.