Comment convertir une image colorée en une image n’ayant que deux couleurs prédéfinies?

J’essaie de convertir une image colorée en une image qui ne comporte que deux couleurs. Mon approche consistait tout d’abord à convertir l’image en une image noir et blanc à l’aide de la classe Aforge.Net Threshold, puis à convertir les pixels en noir et blanc en couleurs que je souhaite. L’affichage étant en temps réel, cette approche introduit un retard important. Je me demandais s’il y avait un moyen plus simple de faire cela.

Bitmap image = (Bitmap)eventArgs.Frame.Clone(); Grayscale greyscale = new Grayscale(0.2125, 0.7154, 0.0721); Bitmap grayImage = greyscale.Apply(image); Threshold threshold = new Threshold(sortinggger); threshold.ApplyInPlace(grayImage); Bitmap colorImage = CreateNonIndexedImage(grayImage); if (colorFilter) { for (int y = 0; y < colorImage.Height; y++) { for (int x = 0; x < colorImage.Width; x++) { if (colorImage.GetPixel(x, y).R == 0 && colorImage.GetPixel(x, y).G == 0 && colorImage.GetPixel(x, y).B == 0) { colorImage.SetPixel(x, y, Color.Blue); } else { colorImage.SetPixel(x, y, Color.Yellow); } } } } private Bitmap CreateNonIndexedImage(Image src) { Bitmap newBmp = new Bitmap(src.Width, src.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (Graphics gfx = Graphics.FromImage(newBmp)) { gfx.DrawImage(src, 0, 0); } return newBmp; } 

La méthode habituelle pour faire correspondre une image à des couleurs spécifiques consiste à utiliser la distance de Pythagore des couleurs dans un environnement 3D avec R, G et B comme axes. Je possède une panoplie d’outils pour manipuler les images et les couleurs, et je ne connais pas trop les frameworks externes. Je vais donc simplement fouiller dans mon contenu et vous donner les fonctions appropriées.

Tout d’abord, le remplacement de la couleur elle-même. Ce code correspond à toute couleur que vous donnez à la couleur disponible la plus proche dans une palette limitée et renvoie l’index dans le tableau donné. Notez que j’ai omis la partie “prendre la racine carrée” du calcul de la distance de Pythagore; nous n’avons pas besoin de connaître la distance réelle, nous avons seulement besoin de les comparer, et cela fonctionne aussi bien sans cette opération plutôt lourde en ressources processeur.

 public static Int32 GetClosestPaletteIndexMatch(Color col, Color[] colorPalette) { Int32 colorMatch = 0; Int32 leastDistance = Int32.MaxValue; Int32 red = col.R; Int32 green = col.G; Int32 blue = col.B; for (Int32 i = 0; i < colorPalette.Length; i++) { Color paletteColor = colorPalette[i]; Int32 redDistance = paletteColor.R - red; Int32 greenDistance = paletteColor.G - green; Int32 blueDistance = paletteColor.B - blue; Int32 distance = (redDistance * redDistance) + (greenDistance * greenDistance) + (blueDistance * blueDistance); if (distance >= leastDistance) continue; colorMatch = i; leastDistance = distance; if (distance == 0) return i; } return colorMatch; } 

Maintenant, sur une image haute en couleurs, cette correspondance de palette devrait être effectuée pour chaque pixel de l’image, mais si votre entrée est déjà palettisée, alors vous pouvez simplement le faire sur la palette de couleurs, ce qui réduit les recherches dans la palette. à seulement 256 par image:

 Color[] colors = new Color[] {Color.Black, Color.White }; ColorPalette pal = image.Palette; for(Int32 i = 0; i < pal.Entries.Length; i++) { Int32 foundIndex = ColorUtils.GetClosestPaletteIndexMatch(pal.Entries[i], palette); pal.Entries[i] = palette[foundIndex]; } image.Palette = pal; 

Et c'est tout; toutes les couleurs de la palette sont remplacées par leur correspondance la plus proche.

Notez que la propriété Palette crée en réalité un nouvel object ColorPalette et ne fait pas référence à celui de l'image. Le code image.Palette.Ensortinges[0] = Color.Blue; ne fonctionnerait pas, car il ne ferait que modifier cette copie non référencée. De ce fait, l’object palette doit toujours être retiré, modifié puis réaffecté à l’image.

Si vous devez enregistrer le résultat sous le même nom de fichier, vous pouvez utiliser une astuce avec un stream , mais si vous avez simplement besoin que la palette de l'object soit modifiée pour que ces deux couleurs soient modifiées, c'est tout.


Si vous n'êtes pas sûr du format d'image d'origine, le processus est un peu plus compliqué:

Comme mentionné précédemment dans les commentaires, GetPixel et SetPixel sont extrêmement lents et il est beaucoup plus efficace d'accéder aux octets sous-jacents de l'image. Cependant, à moins que vous ne soyez sûr à 100% du format de pixel de votre type d'entrée, vous ne pouvez pas simplement accéder à ces octets, car vous devez savoir comment les lire. Une solution simple consiste à laisser le framework faire le travail pour vous, en peignant votre image existante sur une nouvelle image 32 bits par pixel:

 public static Bitmap PaintOn32bpp(Image image, Color? transparencyFillColor) { Bitmap bp = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb); using (Graphics gr = Graphics.FromImage(bp)) { if (transparencyFillColor.HasValue) using (System.Drawing.SolidBrush myBrush = new System.Drawing.SolidBrush(Color.FromArgb(255, transparencyFillColor.Value))) gr.FillRectangle(myBrush, new Rectangle(0, 0, image.Width, image.Height)); gr.DrawImage(image, new Rectangle(0, 0, bp.Width, bp.Height)); } return bp; } 

Maintenant, vous voulez probablement vous assurer que les pixels transparents ne se retrouvent pas sous la forme de la couleur qui se cache derrière une valeur alpha de 0. Vous devez donc spécifier la transparencyFillColor dans cette fonction pour créer un fond permettant de supprimer toute transparence de l'image source. .

Maintenant, nous avons l'image haute-couleur, l'étape suivante consiste à parcourir les octets de l'image, à les convertir en couleurs ARVB et à les faire correspondre à la palette, à l'aide de la fonction que j'ai précédemment donnée. Je conseillerais de créer une image 8 bits, car ce sont les plus faciles à modifier sous forme d'octets, et le fait qu'ils aient une palette de couleurs rend ridiculement facile le remplacement des couleurs après leur création.

Quoi qu'il en soit, les octets. Il est probablement plus efficace pour les gros fichiers de parcourir immédiatement les octets de la mémoire non sécurisée, mais je préfère généralement les copier. Votre choix, bien sûr; Si vous pensez que cela en vaut la peine, vous pouvez combiner les deux fonctions ci-dessous pour y accéder directement. Voici un bon exemple pour accéder directement aux octets de couleur .

 ///  /// Gets the raw bytes from an image. ///  /// The image to get the bytes from. /// Ssortingde of the resortingeved image data. /// The raw bytes of the image public static Byte[] GetImageData(Bitmap sourceImage, out Int32 ssortingde) { BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat); ssortingde = sourceData.Ssortingde; Byte[] data = new Byte[ssortingde * sourceImage.Height]; Marshal.Copy(sourceData.Scan0, data, 0, data.Length); sourceImage.UnlockBits(sourceData); return data; } 

À présent, tout ce que vous avez à faire est de créer un tableau représentant votre image 8 bits, de le parcourir tous les octets sur quatre et de faire correspondre les couleurs que vous obtenez à celles de votre palette. Notez que vous ne pouvez jamais supposer que la longueur en octets réelle d'une ligne de pixels (le pas) est égale à la largeur multipliée par le nombre d'octets par pixel. De ce fait, bien que le code ajoute simplement la taille en pixels au décalage de lecture pour obtenir le pixel suivant sur une ligne, il utilise la foulée pour ignorer des lignes entières de pixels dans les données.

 public static Byte[] Convert32BitTo8Bit(Byte[] imageData, Int32 width, Int32 height, Color[] palette, ref Int32 ssortingde) { if (ssortingde < width * 4) throw new ArgumentException("Stride is smaller than one pixel line!", "stride"); Byte[] newImageData = new Byte[width * height]; for (Int32 y = 0; y < height; y++) { Int32 inputOffs = y * stride; Int32 outputOffs = y * width; for (Int32 x = 0; x < width; x++) { // 32bppArgb: Order of the bytes is Alpha, Red, Green, Blue, but // since this is actually in the full 4-byte value read from the offset, // and this value is considered little-endian, they are actually in the // order BGRA. Since we're converting to a palette we ignore the alpha // one and just give RGB. Color c = Color.FromArgb(imageData[inputOffs + 2], imageData[inputOffs + 1], imageData[inputOffs]); // Match to palette index newImageData[outputOffs] = (Byte)ColorUtils.GetClosestPaletteIndexMatch(c, palette); inputOffs += 4; outputOffs++; } } stride = width; return newImageData; } 

Maintenant, nous avons notre tableau 8 bits. Pour convertir ce tableau en image, vous pouvez utiliser la fonction BuildImage j'ai déjà publiée pour une autre réponse .

Donc, finalement, en utilisant ces outils, le code de conversion devrait ressembler à ceci:

 public static Bitmap ConvertToColors(Bitmap image, Color[] colors) { Int32 width = image.Width; Int32 height = image.Height; Int32 ssortingde; Byte[] hiColData; // use "using" to properly dispose of temporary image object. using (Bitmap hiColImage = PaintOn32bpp(image, colors[0])) hiColData = GetImageData(hiColImage, out ssortingde); Byte[] eightBitData = Convert32BitTo8Bit(hiColData, width, height, colors, ref ssortingde); return BuildImage(eightBitData, width, height, ssortingde, PixelFormat.Format8bppIndexed, colors, Color.Black); } 

Nous y allons votre image est convertie en image palette 8 bits, quelle que soit la palette souhaitée.

Si vous souhaitez réellement faire correspondre le noir et le blanc puis remplacer les couleurs, ce n'est pas un problème non plus; faites simplement la conversion avec une palette contenant uniquement du noir et blanc, puis prenez l’object Palette du bitmap obtenu, remplacez les couleurs qu’il contient et atsortingbuez-le à l’image.

 Color[] colors = new Color[] {Color.Black, Color.White }; Bitmap newImage = ConvertToColors(image, colors); ColorPalette pal = newImage.Palette; pal.Ensortinges[0] = Color.Blue; pal.Ensortinges[1] = Color.Yellow; newImage.Palette = pal;