web-dev-qa-db-fra.com

C # - Alternatives plus rapides à SetPixel et GetPixel pour Bitmaps pour Windows Forms App

J'essaie de m'enseigner le C # et j'ai entendu de diverses sources que les fonctions obtenues et setpixel peuvent être horriblement lentes. Quelles sont certaines des alternatives et l'amélioration des performances est-elle vraiment si importante? Merci d'avance!

Un morceau de mon code pour référence:

public static Bitmap Paint(Bitmap _b, Color f)
{
  Bitmap b = new Bitmap(_b);
  for (int x = 0; x < b.Width; x++) 
  {
    for (int y = 0; y < b.Height; y++) 
    {
      Color c = b.GetPixel(x, y);
      b.SetPixel(x, y, Color.FromArgb(c.A, f.R, f.G, f.B));
    }
  }
  return b;
}
36
purdoo

Le code immédiatement utilisable

public class DirectBitmap : IDisposable
{
    public Bitmap Bitmap { get; private set; }
    public Int32[] Bits { get; private set; }
    public bool Disposed { get; private set; }
    public int Height { get; private set; }
    public int Width { get; private set; }

    protected GCHandle BitsHandle { get; private set; }

    public DirectBitmap(int width, int height)
    {
        Width = width;
        Height = height;
        Bits = new Int32[width * height];
        BitsHandle = GCHandle.Alloc(Bits, GCHandleType.Pinned);
        Bitmap = new Bitmap(width, height, width * 4, PixelFormat.Format32bppPArgb, BitsHandle.AddrOfPinnedObject());
    }

    public void SetPixel(int x, int y, Color colour)
    {
        int index = x + (y * Width);
        int col = colour.ToArgb();

        Bits[index] = col;
    }

    public Color GetPixel(int x, int y)
    {
        int index = x + (y * Width);
        int col = Bits[index];
        Color result = Color.FromArgb(col);

        return result;
    }

    public void Dispose()
    {
        if (Disposed) return;
        Disposed = true;
        Bitmap.Dispose();
        BitsHandle.Free();
    }
}

Il n'y a pas besoin de LockBits ou SetPixel. Utilisez la classe ci-dessus pour un accès direct aux données bitmap.

Avec cette classe, il est possible de définir des données bitmap brutes en tant que données 32 bits. Notez qu'il s'agit de PARGB, qui est un alpha prémultiplié. Voir Alpha Compositing sur Wikipedia pour plus d'informations sur la façon dont cela fonctionne et exemples sur l'article MSDN pour BLENDFUNCTION pour savoir comment calculer correctement l'alpha.

Si la prémultiplication peut compliquer les choses, utilisez PixelFormat.Format32bppArgb au lieu. Un impact sur les performances se produit lorsqu'il est dessiné, car il est converti en interne en PixelFormat.Format32bppPArgb. Si l'image ne doit pas changer avant d'être dessinée, le travail peut être effectué avant la prémultiplication, dessiné sur un PixelFormat.Format32bppArgb buffer, et utilisé à partir de là.

L'accès aux membres standard Bitmap est exposé via la propriété Bitmap. Les données bitmap sont directement accessibles à l'aide de la propriété Bits.

Utilisation de byte au lieu de int pour les données de pixels bruts

Modifiez les deux instances de Int32 à byte, puis modifiez cette ligne:

Bits = new Int32[width * height];

Pour ça:

Bits = new byte[width * height * 4];

Lorsque des octets sont utilisés, le format est Alpha/Rouge/Vert/Bleu dans cet ordre. Chaque pixel prend 4 octets de données, un pour chaque canal. Les fonctions GetPixel et SetPixel devront être retravaillées en conséquence ou supprimées.

Avantages d'utiliser la classe ci-dessus

  • L'allocation de mémoire pour la simple manipulation des données n'est pas nécessaire; les modifications apportées aux données brutes sont immédiatement appliquées au bitmap.
  • Il n'y a aucun objet supplémentaire à gérer. Cela implémente IDisposable comme Bitmap.
  • Il ne nécessite pas de bloc unsafe.

Considérations

  • La mémoire épinglée ne peut pas être déplacée. C'est un effet secondaire requis pour que ce type d'accès à la mémoire fonctionne. Cela réduit l'efficacité du ramasse-miettes ( article MSDN ). Ne le faites qu'avec des bitmaps où les performances sont requises, et assurez-vous de Dispose lorsque vous avez terminé pour que la mémoire puisse être décrochée.

Accès via l'objet Graphics

Étant donné que la propriété Bitmap est en fait un objet .NET Bitmap, il est simple d'effectuer des opérations à l'aide de la classe Graphics.

var dbm = new DirectBitmap(200, 200);
using (var g = Graphics.FromImage(dbm.Bitmap))
{
    g.DrawRectangle(Pens.Black, new Rectangle(50, 50, 100, 100));
}

Comparaison des performances

La question porte sur les performances, voici donc un tableau qui devrait montrer les performances relatives entre les trois différentes méthodes proposées dans les réponses. Cela a été fait en utilisant une application basée sur .NET Standard 2 et NUnit.

* Time to fill the entire bitmap with red pixels *
- Not including the time to create and dispose the bitmap
- Best out of 100 runs taken
- Lower is better
- Time is measured in Stopwatch ticks to emphasize magnitude rather than actual time elapsed
- Tests were performed on an Intel Core i7-4790 based workstation

              Bitmap size
Method        4x4   16x16   64x64   256x256   1024x1024   4096x4096
DirectBitmap  <1    2       28      668       8219        178639
LockBits      2     3       33      670       9612        197115
SetPixel      45    371     5920    97477     1563171     25811013

* Test details *

- LockBits test: Bitmap.LockBits is only called once and the benchmark
                 includes Bitmap.UnlockBits. It is expected that this
                 is the absolute best case, adding more lock/unlock calls
                 will increase the time required to complete the operation.
80
SaxxonPike

La raison pour laquelle les opérations bitmap sont si lentes en C # est due au verrouillage et au déverrouillage. Chaque opération effectue un verrouillage sur les bits requis, manipule les bits, puis déverrouille les bits.

Vous pouvez considérablement améliorer la vitesse en gérant vous-même les opérations. Voir l'exemple suivant.

using (var tile = new Bitmap(tilePart.Width, tilePart.Height))
{
  try
  {
      BitmapData srcData = sourceImage.LockBits(tilePart, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
      BitmapData dstData = tile.LockBits(new Rectangle(0, 0, tile.Width, tile.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

      unsafe
      {
          byte* dstPointer = (byte*)dstData.Scan0;
          byte* srcPointer = (byte*)srcData.Scan0;

          for (int i = 0; i < tilePart.Height; i++)
          {
              for (int j = 0; j < tilePart.Width; j++)
              {
                  dstPointer[0] = srcPointer[0]; // Blue
                  dstPointer[1] = srcPointer[1]; // Green
                  dstPointer[2] = srcPointer[2]; // Red
                  dstPointer[3] = srcPointer[3]; // Alpha

                  srcPointer += BytesPerPixel;
                  dstPointer += BytesPerPixel;
              }
              srcPointer += srcStrideOffset + srcTileOffset;
              dstPointer += dstStrideOffset;
          }
      }

      tile.UnlockBits(dstData);
      aSourceImage.UnlockBits(srcData);

      tile.Save(path);
  }
  catch (InvalidOperationException e)
  {

  }
}
16
Bort

Cela fait un certain temps, mais j'ai trouvé un exemple qui pourrait être utile.

  BitmapData BtmpDt = a.LockBits(new Rectangle(0,0,btm.Width,btm.Height),ImageLockMode.ReadWrite,btm.PixelFormat);
  IntPtr pointer = BtmDt.Scan0;
  int size = Math.Abs(BtmDt.Stride)*btm.Height;
  byte[] pixels = new byte[size];
  Marshal.Copy(pointer,pixels,0, size);
  for (int b = 0; b < pixels.Length; b++) {
    pixels[b] = 255;// do something here 
  }
  Marshal.Copy(pixels,0,pointer, size);
  btm.UnlockBits(BtmDt);

btm est une variable bitmap.

2
user9015994

Vous pouvez utiliser la méthode Bitmap.LockBits. De plus, si vous souhaitez utiliser l'exécution de tâches parallèles, vous pouvez utiliser la classe Parallel dans l'espace de noms System.Threading.Tasks. Les liens suivants contiennent quelques exemples et explications.

1
turgay