Laden...

Bitmap, Graphics, Handle: Problem mit 8-bit-Pixel [Snippet zur Umwandlung von Bildern in 8bpp]

Erstellt von juetho vor 17 Jahren Letzter Beitrag vor 15 Jahren 9.590 Views
J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 17 Jahren
Bitmap, Graphics, Handle: Problem mit 8-bit-Pixel [Snippet zur Umwandlung von Bildern in 8bpp]

Frohes Neues Jahr!

Bei der Erweiterung meines Projekts FormPrint bin ich über ein Problem gestolpert. Mein jetziges Ziel ist, **Bitmaps mit geringerem Speicherbedarf **zu verarbeiten.

Standard bei NET sind 32-bit-Pixel. Damit funktioniert das Kopieren einer Bitmap (wie von Programmierhans vorgeschlagen):

Graphics grCtrl = Graphics.FromHwnd(Ctrl.Handle);
Bitmap Result = new Bitmap(Ctrl.Width, Ctrl.Height, grCtrl);
//  we need handles to copy graphics
IntPtr   hdcCtrl = grCtrl.GetHdc();
Graphics grDest  = Graphics.FromImage(Result);
IntPtr   hdcDest = grDest.GetHdc();
BitBlt(hdcDest, 0, 0, Ctrl.Width, Ctrl.Height, hdcCtrl, 0,0, SRCCOPY);

Um das Druckerproblem "Speicher voll" zu umgehen, möchte ich ggf. 8-bit-Pixel zulassen. Das klappt aber nicht:

Situation 1: PixelFormat ist eine read-only-Eigenschaft, kann also nicht einfach zugeordnet werden.

Situation 2: Wenn ich PixelFormat beim Konstruktor zuweise, erhalte ich bei Graphics.FromImage() eine Exception:

Bitmap Result = new Bitmap(Ctrl.Width, Ctrl.Height, PixelFormat.Format8bppIndexed);

Ein Grafikobjekt kann nicht von einem Bild mit einem indizierten Pixelformat erstellt werden.

Situation 3: Beim Bitmap-Konstruktor (Int32, Int32, Int32, PixelFormat, IntPtr) kann ich das PixelFormat zuordnen, habe aber keine Ahnung (und finde kein Beispiel), wie ich an die Parameter 3 (stride) und 5 (scan0) komme. Außerdem glaube ich, dass dieser Konstruktor für mein Problem nicht passt: Der Parameter scan0 ist laut Doku ein "Zeiger auf ein Array von Bytes, das die Pixeldaten enthält"; aber ich will ja die Pixel erst noch kopieren. Wenn hier der erste Handle hdcCtrl genommen werden kann: wie kann ich stride bestimmen?

Situation 4: Ich hatte zwischenzeitlich angenommen, dass ImageFormat.Gif mein Problem lösen würde. Das stimmt aber offensichtlich nicht, weil auch bei GIF mehr als 256 Farben möglich sind.

Was für Möglichkeiten habe ich noch, um entweder variabel jedes PixelFormat oder (als Ersatz) ein bestimmtes "speicherreduziertes" Pixelformat zu verwenden?

Recht herzlichen Dank! Jürgen

PS. Eigentlich wollte ich mich nicht so ausführlich mit Grafik befassen, weil ich eher mit Datenbanken und kaufmännischer Software zu tun habe. Aber als nützliche Erweiterung eines/jeden Programms will ich es jetzt wenigstens sauber fertigstellen.

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo juetho,

vor einem ähnlichen Problem stand ich neulich. Leider hat das Framework (auch nach Bekunden von Microsoft) in diesem Bereich einige Lücken, die es ziemlich aufwändig machen, damit umzugehen. Hier meine Lösung, namlich das Bild erst am Schluß in 8bpp umwandeln:


//--------------------------------------------------------------------------
// Wandelt ein Bild mit nicht mehr als 256 Farben in ein Bild im Format
// PixelFormat.Format8bppIndexed um. Wenn das Originalbild mehr Farben hat
// wird es unverändert zurückgeliefert.
// Es werden nur die reinen Farbwerte (RGB) ohne den AlphaKanal betrachtet.
// Wenn fTransparent angegeben ist, wird die Farbe des linken oberen Pixels
// auf 100% Transparenz gesetzt.
//--------------------------------------------------------------------------
private static Bitmap To8bppIndexed (Bitmap bmpOld, bool fTransparent)
{
   Dictionary <Color, byte> dictPalette;
   Bitmap     bmpNew;
   BitmapData bmpdNew;
   byte []    abNewPixel;
   Color      clrFirst;

   //-----------------------------------------------------------------------
   // Init
   //-----------------------------------------------------------------------
   dictPalette = new Dictionary <Color, byte> ();

   //-----------------------------------------------------------------------
   // Farben ermitteln
   //-----------------------------------------------------------------------
   for (int iY = 0; iY < bmpOld.Height; ++iY) {
      for (int iX = 0; iX < bmpOld.Width; ++iX) {
         Color clrTmp = Color.FromArgb (255, bmpOld.GetPixel (iX, iY));
         dictPalette [clrTmp] = 0;
      }
   }

   //-----------------------------------------------------------------------
   // Wenn das Bild zu viele Farben hat, ist eine Konvertierung nicht
   // möglich und deshalb wird das Originalbild unverändert
   // zurückgeliefert.
   //-----------------------------------------------------------------------
   if (dictPalette.Keys.Count > 256) {
      return bmpOld;
   }

   //-----------------------------------------------------------------------
   // Zielbild als 8bppIndexed erzeugen
   //-----------------------------------------------------------------------
   bmpNew = new Bitmap (bmpOld.Width,
                        bmpOld.Height,
                        PixelFormat.Format8bppIndexed);

   //-----------------------------------------------------------------------
   // Ermittelte Farben in die Palette des neuen Bildes schreiben
   // Anm: Das "herausholen" der ColorPalette in eine extra Variable
   // und spätere zurückschreiben ist tatsächlich nötig. Das direkte
   // Verändern von bmpNew.Palette.Entries [i] bleibt wirkungslos.
   //-----------------------------------------------------------------------
   int i = 0;
   clrFirst = Color.FromArgb (255, bmpOld.GetPixel (0, 0));
   ColorPalette clrp = bmpNew.Palette;

   foreach (Color clr in new List <Color> (dictPalette.Keys)) {
      dictPalette [clr] = (byte)i;
      if (fTransparent && clr == clrFirst) {
         clrp.Entries [i++] = Color.FromArgb (0, clr);
      } else {
         clrp.Entries [i++] = clr;
      }
   }
   bmpNew.Palette = clrp;

   //-----------------------------------------------------------------------
   // Übertragen der Pixelinformation ins neue Bild
   //-----------------------------------------------------------------------
   bmpdNew = bmpNew.LockBits (new Rectangle (0, 0, bmpNew.Width, bmpNew.Height),
                              ImageLockMode.WriteOnly,
                              bmpNew.PixelFormat);
   abNewPixel = new byte [bmpdNew.Stride * bmpNew.Height];

   Marshal.Copy (bmpdNew.Scan0, abNewPixel, 0, abNewPixel.Length);

   for (int iY = 0; iY < bmpOld.Height; ++iY) {
      for (int iX = 0; iX < bmpOld.Width; ++iX) {
         Color clrTmp = Color.FromArgb (255, bmpOld.GetPixel (iX, iY));
         abNewPixel [iY * bmpdNew.Stride + iX] = dictPalette [clrTmp];
      }
   }

   Marshal.Copy (abNewPixel, 0, bmpdNew.Scan0, abNewPixel.Length);

   bmpNew.UnlockBits (bmpdNew);

   return bmpNew;
}

herbivore

PS: Der Code sollte sich leicht an 1.1 anpassen lassen, wenn du Hashtable statt Dictionary und ArrayList statt List verwendest.

Suchhilfe: 1000 Worte

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 17 Jahren

Original von herbivore

//-----------------------------------------------------------------------  
// Wenn das Bild zu viele Farben hat, ist eine Konvertierung nicht  
// möglich und deshalb wird das Originalbild unverändert  
// zurückgeliefert.  
//-----------------------------------------------------------------------  
if (dictPalette.Keys.Count > 256) {  
   return bmpOld;  
}  

Aber genau diese Situation wäre für mein Problem wichtig (vermutlich als Folge des eigentlichen Problems "zu viele Daten"). Wenn es keinen besseren Weg gibt, werde ich mich auf eine Warnung beschränken müssen.

Weitere Idee, der ich nachgehen werde: das komplexe Bitmap als Temp-Datei speichern, mit Farbreduzierung neu einlesen und dann drucken.

Danke jedenfalls für die Hinweise, dass MS die Lücke zu verantworten hat (und nicht meine fehlenden Grafik-Kenntnisse). Jürgen

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo juetho,

die Farbreduktion war bei mir nicht nötig, da im Prinzip sichergestellt war, dass die Anzahl der Farben sogar weit unter 256 lag.

Wenn man sie braucht sollte man einen der bekannten und bewährten Algorithmen nehmen, siehe z.B. http://de.wikipedia.org/wiki/Farbreduktion

herbivore

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 17 Jahren

Original von herbivore
Wenn man sie braucht sollte man einen der bekannten und bewährten Algorithmen nehmen, siehe z.B.
>

herbivore

Ojemini, und das mir - wo ich gerade mal weiß, dass es 8-bit-Pixel und 32-bit-Pixel gibt und ich mich niemals wirklich damit befassen will...

Trotzdem danke schön für den Hinweis! Jürgen

Nachtrag 1: Ich lese gerade Non-32bpp Images; vielleicht bringt mich das weiter (und vielleicht hast auch Du etwas davon).[/edit]

Nachtrag 2: Ich habe den Eindruck, dass das hier vorgestellte Verfahren: Convert to 8bpp Bitmap eine akzeptable Notlösung wäre (statt jedes Format vorzusehen, würde auf Wunsch nur nach 8-bit-Pixel konvertiert). Mal sehen...[/edit]

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 17 Jahren

Original von juetho
Nachtrag 2: Ich habe den Eindruck, dass das hier vorgestellte Verfahren:
>
eine akzeptable Notlösung wäre (statt jedes Format vorzusehen, würde auf Wunsch nur nach 8-bit-Pixel konvertiert). Mal sehen...[/edit]

So, ich hab das jetzt übernommen - und es funktioniert eigentlich hervorragend. So übernehme ich es jetzt in meine Klasse FormPrint.

Übrig bleibt noch ein praktisches Problem: Die Geschwindigkeit von GetPixel und SetPixel ist katastrophal (eine Fullscreen-Copy 1280x960 benötigt bei mir 65 Sekunden). Was ratet Ihr mir für das weitere Vorgehen?

Aktuelle Lösung:

//  bei Bedarf die Farbdichte reduzieren
if (b8bitPixel)    
	bmp = ConvertTo8bppFormat(bmp);

Diese Convert-Methode habe ich unverändert kopiert aus (Step 2) Convert to 8bpp Bitmap.

Mögliche Variante 1: nur Sanduhr anzeigen
Das ist möglich, ist mir aber für die benötigte Zeit etwas wenig.

Mögliche Variante 2: eigene ProgressBar
Das gefällt mir nicht, weil meine Klasse einfach sein sollte und ich nicht nur für diese eine Situation ein Mini-Formular hinzufügen will.

Mögliche Variante 3: weiterer Parameter mit Verweis auf "externe" ProgressBar oder Label
Das gefällt mir relativ gut: Der Nutzer der FormPrint-Klasse kann selbst entscheiden, wie/wo der Arbeitsfortschritt angezeigt werden soll. Nachteil: Noch ein Parameter beim Aufruf FormPrint.Print() mit entsprechend vielen überladenen Varianten.

Mögliche Variante 4: weiterer Parameter mit Verweis auf einen Delegate
Das gefällt mir ähnlich gut wie Variante 3; Nachteil analog, aber für die Einbindung in das "nutzende" Formular vielleicht noch komplizierter.

Mögliche Variante 5: 800mal schneller als GetPixel und SetPixel
An dieser Stelle scheitern meine Grafik-Kenntnisse endgültig.

Meine Klasse enthält bisher etwa folgenden Code:*100 Zeilen ConvertTo8bppFormat *200 Zeilen eigentlicher Arbeitsablauf *200 Zeilen Aufruf der Klasse, 15 überladene Varianten

Fallen Euch andere Varianten ein, die schnell in meine Klasse eingebaut werden können, ohne sie allzusehr aufblähen? Welche der genannten Varianten würdet Ihr bevorzugen und warum?

Danke für Hinweise! Jürgen

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo juetho,

ich habe oben GetPixel verwendet, weil ich ich wusste, dass in meinem Fall nur sehr kleine Bilder verarbeitet werden. Ich laufe ja auch sogar zweimal mit GetPixel über das ganze Bild. Auch hier läge Optimierungspotential um dem Faktor 2. Statt GetPixel kann und sollte man für große Bilder auch für das Auslesen besser LockBits verwenden.

herbivore

B
1.529 Beiträge seit 2006
vor 17 Jahren

Die saubere Lösung, um den Fortschritt weiterzugeben, ist die Definition eines eigenes Ereignisses.


public class FormPrintProgressEventArgs : EventArgs
{
   public long PixelsTotal;
   public long PixelsDone;
   public FormPrintProgressEventArgs( long Total, long Done ) : base()
   {
      PixelsTotal = Total;
      PixelsDone = Done;
   }
}

public delegate void FormPrintProgressEventHandler( object sender, FormPrintProgressArgs args );

Dann kann die benutzende Klasse entscheiden, was getan werden soll.

R
344 Beiträge seit 2006
vor 17 Jahren

Habe mich mal daran versucht.

GetPixel und SetPixel um Längen geschlagen. 800 mal schneller

Auf < 1 Sekunde bin ich bei 1280 x 998 gekommen.

Dabei habe ich gelernt static zu benutzen.

Gruß Robert

182 Beiträge seit 2006
vor 15 Jahren

und wie würde das ganze dann für 4 bit aussehen?
bzw. will ich das gleiche PixelFormat wie ein X-Beliebiges anderes bild.

Real programmers don't comment.
If it was hard to write, it should be hard to understand!

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo Hufy90,

und wie würde das ganze dann für 4 bit aussehen?

genauso. Nur mit 4 statt 8 und mit 16 statt 256. Die Änderung durchzuführen, ist deine Aufgabe.

bzw. will ich das gleiche PixelFormat wie ein X-Beliebiges anderes bild.

Das wird nicht klappen, weil .NET nur sehr wenige Pixelformate unterstützt. Entweder du beschränkst dich auch diese oder du verwendest eine Grafik-Bibliothek, die das kann.

herbivore

182 Beiträge seit 2006
vor 15 Jahren

Meine Änderungen sehen wie folgt aus:

//--------------------------------------------------------------------------
// Wandelt ein Bild mit nicht mehr als 256 Farben in ein Bild im Format
// PixelFormat.Format8bppIndexed um. Wenn das Originalbild mehr Farben hat
// wird es unverändert zurückgeliefert.
// Es werden nur die reinen Farbwerte (RGB) ohne den AlphaKanal betrachtet.
// Wenn fTransparent angegeben ist, wird die Farbe des linken oberen Pixels
// auf 100% Transparenz gesetzt.
//--------------------------------------------------------------------------
private static Bitmap To4bppIndexed (Bitmap bmpOld, bool fTransparent)
{
   Dictionary <Color, byte> dictPalette;
   Bitmap     bmpNew;
   BitmapData bmpdNew;
   byte []    abNewPixel;
   Color      clrFirst;

   //-----------------------------------------------------------------------
   // Init
   //-----------------------------------------------------------------------
   dictPalette = new Dictionary <Color, byte> ();

   //-----------------------------------------------------------------------
   // Farben ermitteln
   //-----------------------------------------------------------------------
   for (int iY = 0; iY < bmpOld.Height; ++iY) {
      for (int iX = 0; iX < bmpOld.Width; ++iX) {
         Color clrTmp = Color.FromArgb (15, bmpOld.GetPixel (iX, iY));
         dictPalette [clrTmp] = 0;
      }
   }

   //-----------------------------------------------------------------------
   // Wenn das Bild zu viele Farben hat, ist eine Konvertierung nicht
   // möglich und deshalb wird das Originalbild unverändert
   // zurückgeliefert.
   //-----------------------------------------------------------------------
   if (dictPalette.Keys.Count > 16) {
      return bmpOld;
   }

   //-----------------------------------------------------------------------
   // Zielbild als 8bppIndexed erzeugen
   //-----------------------------------------------------------------------
   bmpNew = new Bitmap (bmpOld.Width,
                        bmpOld.Height,
                        PixelFormat.Format4bppIndexed);

   //-----------------------------------------------------------------------
   // Ermittelte Farben in die Palette des neuen Bildes schreiben
   // Anm: Das "herausholen" der ColorPalette in eine extra Variable
   // und spätere zurückschreiben ist tatsächlich nötig. Das direkte
   // Verändern von bmpNew.Palette.Entries [i] bleibt wirkungslos.
   //-----------------------------------------------------------------------
   int i = 0;
   clrFirst = Color.FromArgb (15, bmpOld.GetPixel (0, 0));
   ColorPalette clrp = bmpNew.Palette;

   foreach (Color clr in new List <Color> (dictPalette.Keys)) {
      dictPalette [clr] = (byte)i;
      if (fTransparent && clr == clrFirst) {
         clrp.Entries [i++] = Color.FromArgb (0, clr);
      } else {
         clrp.Entries [i++] = clr;
      }
   }
   bmpNew.Palette = clrp;

   //-----------------------------------------------------------------------
   // Übertragen der Pixelinformation ins neue Bild
   //-----------------------------------------------------------------------
   bmpdNew = bmpNew.LockBits (new Rectangle (0, 0, bmpNew.Width, bmpNew.Height),
                              ImageLockMode.WriteOnly,
                              bmpNew.PixelFormat);
   abNewPixel = new byte [bmpdNew.Stride * bmpNew.Height];

   Marshal.Copy (bmpdNew.Scan0, abNewPixel, 0, abNewPixel.Length);

   for (int iY = 0; iY < bmpOld.Height; ++iY) {
      for (int iX = 0; iX < bmpOld.Width; ++iX) {
         Color clrTmp = Color.FromArgb (15, bmpOld.GetPixel (iX, iY));
         abNewPixel [iY * bmpdNew.Stride + iX] = dictPalette [clrTmp];
      }
   }

   Marshal.Copy (abNewPixel, 0, bmpdNew.Scan0, abNewPixel.Length);

   bmpNew.UnlockBits (bmpdNew);

   return bmpNew;
}

Ich erhalte jedoch hier immer eine IndexOutOfRange-Exception:

abNewPixel [iY * bmpdNew.Stride + iX] = dictPalette [clrTmp];

Real programmers don't comment.
If it was hard to write, it should be hard to understand!

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo Hufy90,

in jedem Byte musst du dann natürlich zwei Pixelindexwerte unterbringen. Siehe [Artikel] Bitoperationen in C#

herbivore

182 Beiträge seit 2006
vor 15 Jahren

OK aber wie? OR, AND, XOR,... oder wie?

Real programmers don't comment.
If it was hard to write, it should be hard to understand!

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo Hufy90,

ersterIndex << 4 | zweiterIndex

Aber wie man zwei Nibbles in ein Byte bekommt, gehört eigentlich zu den Grundlagen, die wir voraussetzen.

herbivore