45 votes

Trouver les couleurs spécifiques des pixels d'une image BitmapImage

J'ai un WPF BitmapImage que j'ai chargé à partir d'un fichier .JPG, comme suit :

this.m_image1.Source = new BitmapImage(new Uri(path));

Je veux savoir quelle est la couleur à des endroits précis. Par exemple, quelle est la valeur RVB au pixel (65,32) ?

Comment dois-je m'y prendre ? J'ai adopté cette approche :

ImageSource ims = m_image1.Source;
BitmapImage bitmapImage = (BitmapImage)ims;
int height = bitmapImage.PixelHeight;
int width = bitmapImage.PixelWidth;
int nStride = (bitmapImage.PixelWidth * bitmapImage.Format.BitsPerPixel + 7) / 8;
byte[] pixelByteArray = new byte[bitmapImage.PixelHeight * nStride];
bitmapImage.CopyPixels(pixelByteArray, nStride, 0);

Bien que j'avoue qu'il y a un peu de "monkey-see, monkey do" dans ce code. Quoi qu'il en soit, existe-t-il un moyen direct de traiter ce tableau d'octets pour le convertir en valeurs RVB ?

0 votes

A quoi sert le nStride ? Et pourquoi ajouter 7 et diviser par 8 dans le calcul de nStride ?

3 votes

@Jviaches Ajoutez 7 et divisez par 8 pour arrondir correctement à un nombre suffisant d'octets (par exemple, 10 bits nécessitent 2 octets).

61voto

Ray Burns Points 38537

Voici comment je manipulerais des pixels en C# en utilisant des tableaux multidimensionnels :

[StructLayout(LayoutKind.Sequential)]
public struct PixelColor
{
  public byte Blue;
  public byte Green;
  public byte Red;
  public byte Alpha;
}

public PixelColor[,] GetPixels(BitmapSource source)
{
  if(source.Format!=PixelFormats.Bgra32)
    source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);

  int width = source.PixelWidth;
  int height = source.PixelHeight;
  PixelColor[,] result = new PixelColor[width, height];

  source.CopyPixels(result, width * 4, 0);
  return result;
}

l'usage :

var pixels = GetPixels(image);
if(pixels[7, 3].Red > 4)
{
  ...
}

Si vous voulez mettre à jour les pixels, un code très similaire fonctionne, sauf que vous créerez un fichier de type WriteableBitmap et utiliser ceci :

public void PutPixels(WriteableBitmap bitmap, PixelColor[,] pixels, int x, int y)
{
  int width = pixels.GetLength(0);
  int height = pixels.GetLength(1);
  bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, width*4, x, y);
}

ainsi :

var pixels = new PixelColor[4, 3];
pixels[2,2] = new PixelColor { Red=128, Blue=0, Green=255, Alpha=255 };

PutPixels(bitmap, pixels, 7, 7);

Notez que ce code convertit les bitmaps en Bgra32 s'ils arrivent dans un format différent. C'est généralement rapide, mais dans certains cas, cela peut être un goulot d'étranglement, auquel cas cette technique serait modifiée pour correspondre plus étroitement au format d'entrée sous-jacent.

Mise à jour

Depuis BitmapSource.CopyPixels n'accepte pas un tableau à deux dimensions, il est nécessaire de convertir le tableau entre une et deux dimensions. La méthode d'extension suivante devrait faire l'affaire :

public static class BitmapSourceHelper
{
#if UNSAFE
  public unsafe static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
  {
    fixed(PixelColor* buffer = &pixels[0, 0])
      source.CopyPixels(
        new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
        (IntPtr)(buffer + offset),
        pixels.GetLength(0) * pixels.GetLength(1) * sizeof(PixelColor),
        stride);
  }
#else
  public static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
  {
    var height = source.PixelHeight;
    var width = source.PixelWidth;
    var pixelBytes = new byte[height * width * 4];
    source.CopyPixels(pixelBytes, stride, 0);
    int y0 = offset / width;
    int x0 = offset - width * y0;
    for(int y=0; y<height; y++)
      for(int x=0; x<width; x++)
        pixels[x+x0, y+y0] = new PixelColor
        {
          Blue  = pixelBytes[(y*width + x) * 4 + 0],
          Green = pixelBytes[(y*width + x) * 4 + 1],
          Red   = pixelBytes[(y*width + x) * 4 + 2],
          Alpha = pixelBytes[(y*width + x) * 4 + 3],
        };
  }
#endif
}

Il y a deux mises en œuvre ici : La première est rapide mais utilise du code non sécurisé pour obtenir un IntPtr vers un tableau (doit être compilé avec l'option /unsafe). La seconde est plus lente mais ne nécessite pas de code non sécurisé. J'utilise la version unsafe dans mon code.

WritePixels accepte les tableaux bidimensionnels, donc aucune méthode d'extension n'est nécessaire.

Modification : comme Jerry l'a fait remarquer dans les commentaires, en raison de la disposition de la mémoire, le tableau bidimensionnel a la coordonnée verticale en premier, en d'autres termes, il doit être dimensionné en tant que Pixels[Hauteur,Largeur] et non Pixels[Largeur,Hauteur] et adressé en tant que Pixels[y,x].

3 votes

Lorsque j'essaie, j'obtiens "Input array is not a valid rank". Avez-vous une idée ?

0 votes

@whitehawk : voir ce post : stackoverflow.com/questions/3418494/

0 votes

@whitehawk : Merci de m'avoir informé de l'erreur. J'ai mis à jour ma réponse pour montrer comment la corriger. Dans mon propre code, j'utilisais en fait une surcharge CopyPixels différente qui prend un IntPtr.

17voto

Habitante Points 163

J'aimerais ajouter à la réponse de Ray que vous pouvez également déclarer la structure PixelColor comme une union :

[StructLayout(LayoutKind.Explicit)]
public struct PixelColor
{
    // 32 bit BGRA 
    [FieldOffset(0)] public UInt32 ColorBGRA;
    // 8 bit components
    [FieldOffset(0)] public byte Blue;
    [FieldOffset(1)] public byte Green;
    [FieldOffset(2)] public byte Red;
    [FieldOffset(3)] public byte Alpha;
}

De cette façon, vous aurez également accès à la BGRA UInit32 (pour un accès ou une copie rapide des pixels), en plus des composants individuels des octets.

16voto

Michael McCloskey Points 1765

L'interprétation du tableau d'octets résultant dépend du format de pixel du bitmap source, mais dans le cas le plus simple d'une image ARGB de 32 bits, chaque pixel sera composé de quatre octets dans le tableau d'octets. Le premier pixel sera interprété de la manière suivante :

alpha = pixelByteArray[0];
red   = pixelByteArray[1];
green = pixelByteArray[2];
blue  = pixelByteArray[3];

Pour traiter chaque pixel de l'image, vous voudrez probablement créer des boucles imbriquées pour parcourir les lignes et les colonnes, en incrémentant une variable d'index par le nombre d'octets de chaque pixel.

Certains types de bitmap combinent plusieurs pixels en un seul octet. Par exemple, une image monochrome contient huit pixels dans chaque octet. Si vous avez besoin de traiter des images autres que celles à 24/32 bits par pixels (les plus simples), je vous suggère de trouver un bon livre qui traite de la structure binaire sous-jacente des bitmaps.

6 votes

Merci, mais... J'ai l'impression que le cadre .NET devrait avoir une interface qui fait abstraction de toute la complexité du codage. Il est évident que la bibliothèque System.Windows.Media sait comment décoder un bitmap, puisqu'elle le rend à l'écran. Pourquoi devrais-je connaître tous les détails de l'implémentation ? Ne puis-je pas utiliser la logique de rendu qui est déjà implémentée d'une manière ou d'une autre ?

2 votes

Je ressens votre douleur. Dans les formulaires Windows, la classe Bitmap dispose des méthodes GetPixel et SetPixel pour récupérer et définir la valeur des pixels individuels, mais ces méthodes sont notoirement lentes et pratiquement inutiles, sauf dans des cas très simples. Vous pouvez utiliser RenderTargetBitmap et d'autres objets de niveau supérieur pour créer et composer des bitmaps, mais à mon avis, il vaut mieux laisser le traitement des images dans le domaine du C++, où l'accès direct aux bits est plus approprié. Bien sûr, vous pouvez faire du traitement d'image en C#/.NET, mais d'après mon expérience, c'est toujours comme si vous essayiez de mettre une cheville carrée dans un trou rond.

11 votes

Je ne suis pas d'accord. À mon avis, C# est un meilleur choix que C++ pour 90% des besoins en traitement d'images. Le langage C# présente de nombreux avantages par rapport au C++, et ses mots-clés "unsafe" et "fixed" vous donnent un accès direct aux bits à la C++ lorsque vous en avez besoin. Dans la plupart des cas, l'implémentation en C# d'un algorithme de traitement d'images sera très similaire en termes de vitesse à celle du C++, mais occasionnellement, elle sera nettement plus lente. Je n'utiliserais le C++ que dans les rares cas où le C# sera moins performant en raison d'optimisations manquées par le compilateur JIT.

6voto

Brent Points 755

J'aimerais améliorer la réponse de Ray - pas assez de représentants pour commenter. >Cette version a le meilleur de la version sécurisée/gérée, et l'efficacité de la version non sécurisée. De plus, je n'ai plus besoin de passer le stride car la documentation .Net de CopyPixels indique qu'il s'agit du stride du bitmap et non du buffer. C'est trompeur, et peut être calculé dans la fonction de toute façon. Puisque le tableau PixelColor doit avoir le même stride que le bitmap (pour pouvoir le faire en un seul appel de copie), il est logique de créer un nouveau tableau dans la fonction également. C'est simple comme bonjour.

    public static PixelColor[,] CopyPixels(this BitmapSource source)
    {
        if (source.Format != PixelFormats.Bgra32)
            source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
        PixelColor[,] pixels = new PixelColor[source.PixelWidth, source.PixelHeight];
        int stride = source.PixelWidth * ((source.Format.BitsPerPixel + 7) / 8);
        GCHandle pinnedPixels = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        source.CopyPixels(
          new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
          pinnedPixels.AddrOfPinnedObject(),
          pixels.GetLength(0) * pixels.GetLength(1) * 4,
              stride);
        pinnedPixels.Free();
        return pixels;
    }

0 votes

Remarque : "Best of managed" ne signifie pas que le code est sûr. Ce code ne s'exécutera pas en toute sécurité (ou du moins je n'ai pas réussi à le faire fonctionner).

1 votes

Pour que cette méthode fonctionne, le tableau doit être [height, width].

3voto

CZahrobsky Points 81

Si vous ne voulez qu'une seule couleur de pixel :

using System.Windows.Media;
using System.Windows.Media.Imaging;
...
    public static Color GetPixelColor(BitmapSource source, int x, int y)
    {
        Color c = Colors.White;
        if (source != null)
        {
            try
            {
                CroppedBitmap cb = new CroppedBitmap(source, new Int32Rect(x, y, 1, 1));
                var pixels = new byte[4];
                cb.CopyPixels(pixels, 4, 0);
                c = Color.FromRgb(pixels[2], pixels[1], pixels[0]);
            }
            catch (Exception) { }
        }
        return c;
    }

0 votes

Merci, très utile pour faire cela dans WPF : functionx.com/vcsharp/gdi/colorselector.htm

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X