Manipuler des pixels


Je trouve régulièrement des questions sur les forums sur la manière d'accélérer la modification d'une image pixel par pixel.

Sous Delphi on pourra évidemment utiliser TCanvas.Pixels[x, y], mais c'est clairement la méthode la plus lente !

procedure TForm1.FormPaint(Sender: TObject);
var
  Bmp: TBitmap;
  x  : Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.Width := 512;
    Bmp.Height := 512;
    for x := 0 to 511 do
      Bmp.Canvas.Pixels[x, x] := clRed;
    Canvas.Draw(0, 0, Bmp);
  finally
    Bmp.Free;
  end;
end;

On lui préférera un accès direct au buffer interne du bitmap (le DIB = Device Independant Bitmap) via la propriété ScanLine.

procedure TForm1.FormPaint(Sender: TObject);
var
  Bmp   : TBitmap;
  x     : Integer;
  Pixels: PCardinal;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.Width := 512;
    Bmp.Height := 512;
    Bmp.PixelFormat := pf32Bit; // force le format interne du DIB
    for x := 0 to 511 do
    begin
      //Bmp.Canvas.Pixels[x, x] := clRed;
      Pixels := Bmp.ScanLine[x]; // la ligne x
      Inc(Pixels, x); // le xième pixel de la ligne
      Pixels^ := $ff0000; // couleur native RRGGBB
    end;
    Canvas.Draw(0, 0, Bmp);
  finally
    Bmp.Free;
  end;
end;

ici on ne modifiée qu'un pixel par ligne, le gain n'est pas forcément remarquable (d'autant qu'on ne modifie que 512 pixels), mais je voulais simplement montrer le principe. Car en fait, cet article est au sujet d'une API qui existe depuis fort longtemps et qui permet de dessiner un tableau de Pixels !

SetDIBitsToDevice


Pour le coup on va faire une modification plus importante. L'image est déclarée comme un simple tableau de TColor, on accède donc directement aux pixels par pixels[y, x]. Ensuite on déclare le format de ce DIB dans un TBitmapInfo, soit les dimensions et le nombre de bits (32). Et il ne reste plus qu'à envoyer notre tableau de pixels sur un hDC, c'est à dire le Handle d'un Canvas et le tour est joué !

Petite remarque, les DIB ont naturellement la tête en bas, si vous ne voulez pas inverser la coordonnée Y de votre tableau, vous pouvez indiquer une hauteur négative dans TBitmapInfo !

Modification du 29/05/2013: inversion de X et Y sur le tableau de pixels, il faut utiliser pixels[y, x] et non pixels[x, y] !

const
  IMAGE_WIDTH  = 256;
  IMAGE_HEIGHT = 256;
var
  pixels: array[0..IMAGE_HEIGHT - 1, 0..IMAGE_WIDTH - 1] of TColor; // hauteur puis largeur, et non l'inverse

procedure TForm1.FormCreate(Sender: TObject);
var
  x, y: Integer;
begin
  for x := 0 to IMAGE_WIDTH - 1 do
    for y := 0 to IMAGE_HEIGHT - 1 do
      pixels[y, x] := (x + y) or ((x - y) shl 8 ) or ((x + x) shl 16);
end;

procedure TForm1.FormPaint(Sender: TObject);
var
  BitsInfos: TBitmapInfo;
begin
  FillChar(BitsInfos, SizeOf(BitsInfos), 0);
  with BitsInfos do
  begin
    with bmiHeader do
    begin
      biSize          := 40;
      biWidth         := IMAGE_WIDTH;
      biHeight        := - IMAGE_HEIGHT; // inverser le DIB pour avoir le tête en haut
      biPlanes        := 1;
      biBitCount      := 32;
    end;
  end;
  SetDIBitsToDevice(
    Canvas.Handle,
    10, 10, IMAGE_WIDTH, IMAGE_HEIGHT,
    0, 0, 0, IMAGE_HEIGHT,
    @pixels,
    BitsInfos,
    0
  );
end;


Cette même méthode pourra être utilisée pour dessiner le DIB sur un Bitmap sur lequel vous pourrez intervenir via son Canvas pour ajouter du texte par exemple. En voici la preuve:
procedure TForm1.FormPaint(Sender: TObject);
var
  BitsInfos: TBitmapInfo;
  Bmp: TBitmap;
begin
  FillChar(BitsInfos, SizeOf(BitsInfos), 0);
  with BitsInfos do
  begin
    with bmiHeader do
    begin
      biSize          := 40;
      biWidth         := IMAGE_WIDTH;
      biHeight        := - IMAGE_HEIGHT;
      biPlanes        := 1;
      biBitCount      := 32;
    end;
  end;
  Bmp := TBitmap.Create;
  try
    Bmp.width := IMAGE_WIDTH;
    Bmp.Height := IMAGE_HEIGHT;
    Bmp.PixelFormat := pf32Bit;
    SetDIBitsToDevice(
      Bmp.Canvas.Handle,
      0, 0, IMAGE_WIDTH, IMAGE_HEIGHT,
      0, 0, 0, IMAGE_HEIGHT,
      @pixels,
      BitsInfos,
      0
    );
    with Bmp.Canvas do
    begin
      Brush.Style := bsClear;
      Font.Name := 'Arial';
      Font.Size := 12;
      Font.Style := [fsBold];
      TextOut(11, 11, 'Hello World');
      Font.Color := clWhite;
      TextOut(10, 10, 'Hello World');
    end;
    Canvas.Draw(10, 10, Bmp);
  finally
    Bmp.Free;
  end;
end;


img/SetDIBitsToDevice.png
Date de dernière modification : 29/05/2013