MSDN Magazin > Home > Ausgaben > 2008 > Juni >  Foundations: Bitmaps und Pixelbits
Foundations
Bitmaps und Pixelbits
Charles Petzold

Der Code kann hier heruntergeladen werden: Foundations2008_06.exe (159 KB)
Code online durchsuchen

Das Retained Mode-Grafiksystem von Windows® Presentation Foundation (WPF) hat die Grafikprogrammierung in Windows revolutioniert. Programme müssen nun nicht länger ihr visuelles Erscheinungsbild auf dem Bildschirm neu erstellen, wenn das System es verlangt. Das Kompositionssystem behält alle grafischen Abbildungen bei und arrangiert sie in einer vollständigen visuellen Präsentation.
Durch Retained Mode-Grafiken wird die Arbeit sicher vereinfacht, doch Bequemlichkeit an sich war für Windows-Programmierer noch nie ein vorrangiges Anliegen. Erst durch die Kombination des Retained Mode-Grafiksystems mit Benachrichtigungsmechanismen wie Abhängigkeitseigenschaften kommt die Flexibilität und Leistungsfähigkeit von WPF zum Tragen. Grafische Objekte wie Pfade und Pinsel scheinen innerhalb des Kompositionssystems „lebendig“ zu bleiben und reagieren weiter auf Eigenschaftsänderungen und Grafiktransformationen. Folglich können diese Objekte Ziele von Datenbindungen und Animationen sein.
Kürzlich habe ich entdeckt, dass WPF-Bitmaps über eine ähnliche dynamische Qualität verfügen. Eine gerenderte Bitmap reagiert weiterhin auf Änderungen, und zwar nicht nur auf Grafiktransformationen (dies wussten wir bereits), sondern auch auf Änderungen in den eigentlichen Pixelbits innerhalb der Bitmap.
Die zwei Bitmapklassen, die diese dynamische Reaktionsfähigkeit erkennen lassen, sind „RenderTargetBitmap“ und „WriteableBitmap“. Sie gehören zu den neun Klassen, die von „BitmapSource“ abgeleitet werden, der abstrakten Klasse, die die Grundlage für die Bitmapunterstützung in WPF bildet. Unabhängig davon, wie ein Programm eines dieser Bitmapobjekte verwendet (ob es mit einem Image-Element angezeigt oder mit der ImageBrush-Klasse in einen Kacheleffekt verwandelt oder mit der ImageDrawing-Klasse als Teil einer größeren Zeichnung dargestellt wird, vielleicht mit Vektorgrafiken gemischt), wird die Bitmap nie einfach nur gerendert und dann vergessen. Die Bitmap bleibt im visuellen Kompositionssystem dynamisch und ist auch weiterhin in der Lage, auf Anwendungsänderungen zu reagieren.
Virtual Lab: Bitmaps, Pixel und WPF
WPF-Bitmaps reagieren auf Änderungen innerhalb der Bitmap, sowohl über Grafiktransformationen als auch Pixeländerungen. In diesem Virtual Lab können Sie mit den Klassen „RenderTargetBitmap“ und „WriteableBitmap“ experimentieren, die Bitmaps befähigen, auf Änderungen in Ihrer Anwendung zu reagieren. Die Umgebung wurde bereits eingerichtet, einschließlich des Beispielcodes von Charles Petzold. Sie können also sofort mit dem Codieren beginnen, während Sie diese Ausgabe der Foundations-Rubrik lesen.

Experimentieren Sie im Virtual Lab:
Verwenden von „RenderTargetBitmap“
„RenderTargetBitmap“ ist eine Bitmap, auf die Sie praktisch zeichnen können, indem Sie Objekte des Typs „Visual“ auf ihre Oberfläche übertragen. Die einzige Möglichkeit, ein neues Objekt des Typs „RenderTargetBitmap“ zu erstellen, besteht darin, einen Konstruktor zu verwenden, der die Pixeldimensionen der Bitmap, die horizontale und vertikale Auflösung in Punkten pro Zoll (dpi) und ein Objekt des Typs „PixelFormat“ erfordert.
In Kürze gehe ich ausführlicher auf die PixelFormat-Struktur und die verwandte statische PixelFormats-Klasse ein. Zum Erstellen von Objekten des Typs „RenderTargetBitmap“ müssen Sie entweder „PixelFormats.Default“ oder „PixelFormats.Pbgra32“ als letztes Argument für den RenderTargetBitmap-Konstruktor verwenden. In jeden Fall wird eine Bitmap mit 32 Bits pro Pixel und Transparenz erstellt.
Anfangs ist das RenderTargetBitmap-Objekt vollständig transparent. Sie können dann auf die Bitmap zeichnen, indem Sie die Render-Methode mit einem Objekt des Typs „Visual“ aufrufen (einschließlich Klassen, die von „Visual“ abgeleitet sind, z. B. „FrameworkElement“ und „Control“). Durch Aufrufen von „Clear“ können Sie das vollständig transparente Bild wiederherstellen. Wenn die Bitmap gerade angezeigt wird, werden diese Aufrufe in der angezeigten Bitmap ohne weitere Umstände widergespiegelt.
Abbildung 1 zeigt ein kleines, komplettes Programm, das „RenderTargetBitmap“ veranschaulicht. Das Programm erstellt eine Bitmap, die 1.200 Pixel breit und 900 Pixel hoch ist. Diese werden zu den Eigenschaften „PixelWidth“ und „PixelHeight“ des Bitmapobjekts. Da die Breite jedes Pixels 4 Bytes beträgt, belegt die Bitmap mehr als 4 Megabyte Speicherplatz.
class RenderTargetBitmapDemo : Window
{
    RenderTargetBitmap bitmap;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new RenderTargetBitmapDemo());
    }
    public RenderTargetBitmapDemo()
    {
        Title = "RenderTargetBitmap Demo";
        SizeToContent = SizeToContent.WidthAndHeight;
        ResizeMode = ResizeMode.CanMinimize;

        // Create RenderTargetBitmap
        bitmap = new RenderTargetBitmap(1200, 900, 300, 300, 
                                        PixelFormats.Default);

        // Create Image for bitmap
        Image img = new Image();
        img.Stretch = Stretch.None;
        img.Source = bitmap;
        Content = img;
    }
    protected override void  OnMouseDown(MouseButtonEventArgs args)
    {
        Point ptMouse = args.GetPosition(this);
        Random rand = new Random();
        Brush brush = new SolidColorBrush(
            Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256), 
                                                (byte)rand.Next(256)));

        DrawingVisual vis = new DrawingVisual();
        DrawingContext dc = vis.RenderOpen();
        dc.DrawEllipse(brush, null, ptMouse, 12, 12);
        dc.Close();

        bitmap.Render(vis);
    }
    protected override void OnClosed(EventArgs args)
    {
        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bitmap));
        FileStream stream = new FileStream("RenderTargetBitmapDemo.png", 
                                      FileMode.Create, FileAccess.Write);
        enc.Save(stream);
        stream.Close();
    }
}
Beim Aufruf des Konstruktors „RenderTargetBitmap“ wird außerdem eine Auflösung von 300 Punkten pro Zoll (dpi) angegeben. Die Kombination der Pixeldimensionen und der Auflösung führt zur Erstellung einer Bitmap, die 4 Zoll breit und 3 Zoll hoch ist. Das geräteunabhängige WPF-Koordinatensystem arbeitet mit 96 Einheiten pro Zoll. Daher hat die Bitmap eine geräteunabhängige Breite von 384 Einheiten und eine Höhe von 288 Einheiten. Dies sind die Zahlen, die Sie sehen, wenn Sie die von „BitmapSource“ definierten Width- und Height-Eigenschaften untersuchen.
Das RenderTargetBitmapDemo-Programm verwendet das Image-Element, um die Bitmap anzuzeigen, ohne deren Größe anzupassen. Außerdem fängt es MouseDown-Ereignisse auf. Für jeden Mausklick erstellt das Programm ein DrawingVisual-Objekt in Form eines kleinen gefüllten Kreises von ¼ Zoll Durchmesser und ruft dann die Render-Methode auf, um es dem Bitmapbild hinzuzufügen. Der Kreis ist dann in der angezeigten Bitmap zu sehen. Sie können sich dieses Programm wie ein primitives Zeichenprogramm vorstellen, das Rendering und Speicherfunktion in einer einzelnen Bitmap kombiniert.
Sicher wissen Sie (oder können sich denken), dass das Image-Element eine Bitmap anzeigt, indem es die DrawImage-Methode des DrawingContext-Objekts aufruft, das während seiner OnRender-Methode übergeben wird. Die Image-Klasse erhält keine wiederholten Aufrufe ihrer OnRender-Methode, wenn das RenderTargetBitmapDemo-Programm die Bitmap ändert. Diese Änderungen der Bitmap finden auf einer viel tieferen Ebene im visuellen Kompositionssystem statt.
Das RenderTargetBitmapDemo-Programm speichert die resultierende Bitmap in einem PNG-Dateiformat, wenn das Programm beendet wird. Wenn Sie die Bitmap untersuchen, die aus „RenderTargetBitmapDemo“ gespeichert wird, werden Sie feststellen, dass sie 1.200 x 900 Pixel groß ist und dass der Durchmesser jedes Kreises 75 Pixel beträgt, was in Einheiten von 300 Punkten pro Zoll (dpi) ¼ Zoll entspricht.
Verwenden von „WriteableBitmap“
Die WriteableBitmap-Klasse besitzt zwei Konstruktoren. Einer davon ähnelt in hohem Maße dem RenderTargetBitmap-Konstruktor. Die ersten vier Argumente sind die Pixeldimensionen der Bitmap und die Auflösung in Punkten pro Zoll (dpi). Das fünfte Argument ist ein PixelFormat-Objekt, das Ihnen jedoch wesentlich mehr Flexibilität gibt als „RenderTargetBitmap“. Der WriteableBitmap-Konstruktor besitzt einen zusätzlichen Parameter für eine Farbpalette für diejenigen Bitmapformate, die einen solchen erfordern.
Die Pixel von „WriteableBitmap“ werden auf Null initialisiert. Was dies bedeutet, hängt vom Pixelformat ab. In vielen Fällen ist die Bitmap völlig schwarz. Wenn die Bitmap Transparenz unterstützt, ist sie transparent. Wenn die Bitmap eine Farbpalette besitzt, wird der gesamten Bitmap die erste Farbe in der Palette zugewiesen.
Die Änderung von „WriteableBitmap“ unterscheidet sich in hohem Maße von der Änderung von „RenderTargetBitmap“. Für „WriteableBitmap“ müssen Sie eine Methode namens „WritePixels“ aufrufen, die tatsächliche Pixelbits aus einem lokalen Array in die Bitmap kopiert. Natürlich ist es entscheidend, das Format und die Größe der Daten im Array auf die Dimensionen und das Pixelformat der Bitmap abzustimmen.
Beginnen wir mit einem relativ einfachen Beispiel. Das Programm „AnimatedBitmapBrush.cs“ in Abbildung 2 erstellt eine „WriteableBitmap“ und verwendet diese als Basis für einen unterteilten „ImageBrush“, der auf die Background-Eigenschaft des Fensters eingestellt wird. Das Programm stellt dann einen Timer auf 100 Millisekunden ein und ruft wiederholt „WritePixels“ auf, um die Bitmap zu ändern.
class AnimatedBitmapBrush : Window
{
    const int COLS = 48;
    const int ROWS = 48;

    WriteableBitmap bitmap;
    byte[] pixels = new byte[COLS * ROWS];
    byte pixelLevel = 0x00;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new AnimatedBitmapBrush());
    }
    public AnimatedBitmapBrush()
    {
        Title = "Animated Bitmap Brush";
        Width = Height = 300;

        bitmap = new WriteableBitmap(COLS, ROWS, 96, 96, 
                                     PixelFormats.Gray8, null);
        ImageBrush brush = new ImageBrush(bitmap);
        brush.TileMode = TileMode.Tile;
        brush.Viewport = new Rect(0, 0, COLS, ROWS);
        brush.ViewportUnits = BrushMappingMode.Absolute;
        Background = brush;

        DispatcherTimer tmr = new DispatcherTimer();
        tmr.Interval = TimeSpan.FromMilliseconds(100);
        tmr.Tick += TimerOnTick;
        tmr.Start();
    }

    void TimerOnTick(object sender, EventArgs args)
    {
        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;

                double distanceFromCenter = 
                    2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS,
                                 Math.Abs(col - COLS / 2.0) / COLS);
                pixels[index] =
                    (byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
            }

        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels, COLS, 0);
        pixelLevel++;
    }
}
Die konstanten COLS- und ROWS-Werte definieren die Pixeldimensionen dieser Bitmap. Dieselben Werte werden für das Viewport-Rechteck mit Kacheleffekt und außerdem im Tick-Ereignishandler für den Timer verwendet. Das Pixelformat wird auf „PixelFormats.Gray8“ eingestellt, was bedeutet, dass jedes Pixel in der Bitmap von einem 8-Bit-Wert repräsentiert wird, der eine Grauschattierung anzeigt – der Pixelwert 0x00 ist schwarz und 0xFF ist weiß. Das letzte Argument für den WriteableBitmap-Konstruktor ist auf Null gesetzt, weil das Gray8-Format keine Palette erfordert.
Da jedes Pixel 1 Byte groß ist, wird die Dimension des Bytearrays, das ich „pixels“ genannt habe, einfach durch Multiplikation von COLS × ROWS errechnet. Dieses Pixelarray ist als Feld definiert, sodass es nicht in jedem Aufruf des Tick-Ereignishandlers neu erstellt werden muss. Die Daten im Array müssen mit der obersten Zeile von links nach rechts beginnen, dann folgt die zweite Zeile und so weiter. Der Tick-Ereignishandler verfügt über zwei Schleifen für die Zeilen und Spalten der Bitmap, kombiniert diese beiden Werte jedoch in einem eindimensionalen Arrayindex:
   int index = row * COLS + col;
„WritePixels“ akzeptiert keine mehrdimensionalen Arrays. Das erste Argument für „WritePixels“ ist eine Int32Rect-Struktur in Einheiten von Pixelkoordinaten zur Angabe der rechteckigen Teilmenge der Bitmap, die aktualisiert werden soll. Die X- und Y-Eigenschaften des Int32Rect-Objekts beschreiben die Koordinaten der oberen linken Ecke des Rechtecks im Verhältnis zur oberen linken Ecke der Bitmap. Die Width- und Height-Eigenschaften geben die Pixeldimensionen dieses Rechtecks an. Zum Aktualisieren der gesamten Bitmap legen Sie für X und Y den Wert 0 und für „Width“ und „Height“ die Bitmapeigenschaften „PixelWidth“ und „PixelHeight“ fest. Die beiden letzten Argumente für „WritePixels“ werden in Kürze besprochen.
Ich muss gestehen, dass ich die Absicht hatte, ein ganz anderes animiertes Muster für diese Bitmap zu codieren, doch dasjenige, auf das ich hier gestoßen bin, scheint recht unterhaltsam (zumindest in kleinen Dosen). Eines der Bilder wird in Abbildung 3 gezeigt.
Abbildung 3 Anzeige von AnimatedBitmapBrush
Pixelarrays
Das Aufrufen von „WritePixels“ kann sich komplizierter gestalten, wenn die Bitmap kein Ein-Byte-pro-Pixel-Format besitzt und wenn nur eine rechteckige Teilmenge der Bitmap aktualisiert wird. Dieses wichtige Verfahren sollte unbedingt erlernt werden. Das gleiche Arrayformat von Pixelbits muss nämlich in der statischen BitmapSource.Create-Methode verwendet werden, um eine neue Bitmap zu erstellen, und in den CopyPixels-Methoden von „BitmapSource“, um Pixelbits aus einer Bitmap in ein Array zu kopieren. In allen drei Methoden können Sie stattdessen auch ein IntPtr-Element verwenden, um auf einen lokalen Puffer zu verweisen, ich werde mich jedoch auf den Arrayansatz konzentrieren.
Die Gesamtanzahl der Pixel in der Bitmap ist das Produkt der PixelWidth- und PixelHeight-Eigenschaften, die von der BitmapSource-Klasse definiert werden. „BitmapSource“ definiert außerdem eine Format-Abrufeigenschaft des Typs „PixelFormat“, die ihrerseits eine Abrufeigenschaft namens „BitsPerPixel“ definiert, die zwischen 1 und 128 liegen kann. An dem einem Ende speichert ein einzelnes Byte Daten für 8 fortlaufende Pixel, am anderen Ende sind für jedes Pixel 16 Byte Daten erforderlich. Für die Pixelbits beschränken Sie sich vermutlich auf Arrays des Typs „byte“, „ushort“, „uint“ oder „float“.
Das Int32Rect-Objekt, das Sie der WritePixels-Methode bereitstellen, definiert eine rechteckige Teilmenge innerhalb der Bitmap. Die Anzahl der Bytes im Pixelarray muss ausreichend Daten für die Anzahl der Zeilen und Spalten enthalten, die vom Int32Rect-Objekt angegeben werden. Hier wird es ein wenig kompliziert, da verschiedene Pixelformate mehrere Pixel in einem einzelnen Byte speichern. Bei diesen Formaten muss jede Zeile von Daten an einer Bytegrenze beginnen.
Beispiel: Angenommen, Sie arbeiten mit einer Bitmap mit einem Format von 4 Bits pro Pixel. Der rechteckige Bereich der Bitmap, auf den Sie zugreifen oder den Sie aktualisieren, ist 5 Pixel breit und 12 Pixel hoch. Dies sind die Width- und Height-Eigenschaften des Int32Rect-Objekts, das Sie bereitstellen. Das erste Byte im Array enthält Daten für die ersten zwei Pixel, das zweite für die nächsten beiden Pixel, das dritte Byte jedoch enthält nur Daten für das fünfte Pixel in der ersten Zeile. Das nächste Byte entspricht den ersten beiden Pixeln der zweiten Zeile.
Jede Zeile Daten erfordert 3 Bytes, und der gesamte rechteckige Bereich erfordert 36 Bytes. Für diese Berechnung erfordert die WritePixels-Methode ein Argument namens „stride“. Dies ist die Anzahl der Bytes für jede Zeile Pixeldaten. Allgemein wird „stride“ wie folgt berechnet:
int stride = (width * bitsPerPixel + 7) / 8;
Die Breite ist gleich der Width-Eigenschaft der Int32Rect-Struktur. Selbst wenn Sie ein Array des Typs „ushort“, „uint“ oder „float“ verwenden, wird der stride-Wert immer in Bytes angegeben. Sie können dann die Gesamtzahl der Bytes im Array wie folgt berechnen:
  int dimension = height * stride;
Nehmen Sie eine Teilung durch 2, 4 oder 8 vor, wenn Sie ein Array des Typs „ushort“, „uint“ bzw. „float“ verwenden.
Sicher erinnern Sie sich, dass die Windows-API erfordert, dass jede Zeile Bitmapdaten an einer 32-Bit-Speichergrenze beginnt. Daher musste „stride“ ein Vielfaches von 4 sein. Dies ist in WPF nicht erforderlich. Sie können jedoch für „stride“ einen größeren Wert festlegen als den, der mit der Formel berechnet wird, wenn dies für Sie zweckmäßig ist. Vielleicht arbeiten Sie mit einer Bitmap mit einem Byte pro Pixel, Ihr Array ist jedoch vom Typ „uint“ statt „byte“. In diesem Fall speichert jedes Element im Array vier Pixel. Eventuell möchten Sie jede Zeile des Arrays an einer Einheitsgrenze beginnen, obwohl dies nicht unbedingt erforderlich ist, da ausgerichtete Kopien auf vielen aktuellen Hardwareplattformen in der Regel schneller sind als nicht ausgerichtete Kopien.
Bitmap-Pixelformate
Jedes Pixel in einer Bitmap wird von einem oder mehreren Bits repräsentiert, die die Farbe dieser Bitmap definieren. In WPF wird ein bestimmtes Pixelformat von einem Objekt des Strukturtyps „PixelFormat“ repräsentiert. Die statische PixelFormats-Klasse definiert 26 statische Eigenschaften des Typs „PixelFormat“, das Sie beim Erstellen einer Bitmap verwenden können. Diese werden in Abbildung 4 gezeigt, unterteilt in zwei Gruppen: beschreibbare Formate und nicht beschreibbare Formate. Mit drei Ausnahmen (Bgr555, Bgr565 und Bgr101010) entsprechen die Zahlen im Eigenschaftsnamen der Anzahl der Bits pro Pixel.
Beschreibbare Formate
Indexed1
Indexed2
Indexed4
Indexed8
 
BlackWhite
Gray2
Gray4
Gray8
 
Bgr555
Bgr565
 
Bgr32
Bgra32
Pbgra32
Nicht beschreibbare Formate
Default
 
Bgr24
Rgb24
Bgr101010
Cmyk32
 
Gray16
Rgb48
Rgba64
Prgba64
 
Gray32Float
Rgb128Float
Rgba128Float
Prgba128Float
Beim Erstellen einer Bitmap mit „BitmapSource.Create“ können Sie jede der statischen Eigenschaften der PixelFormats-Klasse mit Ausnahme von „PixelFormats.Default“ verwenden. Beim Erstellen einer Bitmap des Typs „WriteableBitmap“ können Sie nur die beschreibbaren Formate verwenden.
Die Formate, die mit dem Wort „Indexed“ beginnen, sind diejenigen, die ein ColorPalette-Objekt in der statischen BitmapSource-Methode oder im WriteableBitmap-Konstruktor erfordern. Jedes Pixel ist ein Index in das ColorPalette-Objekt, daher sind diesen vier Formaten höchstens 2, 4, 16 bzw. 256 Farben zugeordnet. Die Größe von „ColorPalette“ muss nicht der maximalen Anzahl der Farben entsprechen, wenn die eigentlichen Pixelbits nicht ihre maximale Anzahl erreichen.
Bei Formaten von weniger als 8 Bits pro Pixel entsprechen die wichtigsten Bits in einem Byte den Pixeln ganz links. Beispiel: Beim Indexed2-Format ist ein Byte von 0xC9 identisch mit dem Binärwert 11001001 und entspricht vier 2-Bit-Werten von 11, 00, 10 und 01. Diese wiederum entsprechen der vierten, ersten, dritten und zweiten Farbe in der ColorPalette-Kollektion.
Die Formate „BlackWhite“, „Gray2“, „Gray4“ und „Gray8“ sind Bitmaps mit Grauschattierung mit 1, 2, 4 oder 8 Bits pro Pixel. Ein Pixel mit ausschließlich Nullen ist schwarz und ein Pixel mit ausschließlich Einsen ist weiß.
Die übrigen fünf beschreibbaren Formate sind Farbenformate. Die Buchstaben B, G und R stehen für die Grundfarben Blau, Grün und Rot. Der Buchstabe A steht für Alphakanal und gibt an, dass die Bitmap Transparenz unterstützt. Der Buchstabe P steht für integriertes (premultiplied) Alpha. Darauf komme ich in Kürze zurück.
Die Formate „Bgr555“ und „Bgr565“ erfordern beide 16 Bits (oder 2 Bytes) pro Pixel. Das Format „Bgr555“ verwendet 5 Bits für jede Grundfarbe (was 32 Abstufungen ermöglicht), wobei ein Bit übrig bleibt. Wenn die Bits der Grundfarbe Blau durch B0 (das unwichtigste Bit) bis B4 (am wichtigsten) dargestellt werden, ähnlich für Grün und Rot, werden die drei Primärfarben von zwei fortlaufenden Datenbytes gespeichert (siehe Abbildung 5).
Abbildung 5 Zwei-Byte-Pixelformat (Klicken Sie auf das Bild, um es zu vergrößern)
Beachten Sie, dass die grünen Bits über 2 Bytes verteilt sind. Diese Anordnung ergibt viel mehr Sinn, wenn Sie erkennen, dass es sich bei dem Pixel in Wahrheit um eine 16-Bit-Ganzzahl ohne Vorzeichen handelt, wobei das unwichtigste Byte als Erstes gespeichert wird. Die Darstellung in Abbildung 6 zeigt, wie die Grundfarben in einer einzigen kurzen Ganzzahl codiert sind.
Abbildung 6 Pixelformat: kurze Ganzzahl (Klicken Sie auf das Bild, um es zu vergrößern)
Damit Sie sich vergewissern können, dass dies der Fall ist, erstellt das Gradient555Demo-Programm in Abbildung 7 eine Bitmap dieses Formats und schreibt Pixel hinein, die einen Farbverlauf von Blau auf der linken nach Grün auf der rechten Seite anzeigen. Beachten Sie, dass die Dimension des ushort-Arrays nur das Produkt aller Zeilen und Spalten ist.
class Indexed2Demo : Window
{
    const int COLS = 50;
    const int ROWS = 20;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new Indexed2Demo());
    }
    public Indexed2Demo()
    {
        Title = "Bgr555 Bitmap Demo";

        WriteableBitmap bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
                                        PixelFormats.Bgr555, null);

        ushort[] pixels = new ushort[ROWS * COLS];

        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;
                int blue = (COLS - col) * 0x1F / COLS;
                int green = col * 0x1F / COLS;
                ushort pixel = (ushort)(green << 5 | blue);
                pixels[index] = pixel;
            }

        int stride = (COLS * bitmap.Format.BitsPerPixel + 7) / 8;
        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels,
                           stride, 0);

        Image img = new Image();
        img.Source = bitmap;
        Content = img;
    }
}
Dieser Code veranschaulicht den Vorteil der Verwendung eines Arrays eines anderen Typs als „byte“, der der Anzahl der Bytes pro Pixel entspricht. Für die Pixeladresse jeder Zeile und Spalte ergibt sich der Arrayindex ganz einfach als Summe aus der Spalte und dem Produkt aus Zeile mal Anzahl der Pixel pro Spalte.
Das Format „Bgr565“ ist dem Format „Bgr555“ sehr ähnlich. Der Unterschied besteht darin, dass es 6 Bits für Grün verwendet (vom Auge am besten wahrgenommen). Die Arbeit mit den übrigen drei beschreibbaren Formaten gestaltet sich wesentlich einfacher. Alle verwenden 4 Bytes pro Pixel, beginnend mit Blau. Im Format „Bgr32“ ist das letzte der 4 Bytes null, es gibt keine Transparenz. In den beiden anderen Formaten ist das vierte Byte der Alphakanal. Der Alphawert reicht von 0x00 für transparent bis hin zu 0xFF für deckend. Wenn das Pixel wie eine 32-Bit-Ganzzahl ohne Vorzeichen behandelt wird, codieren die unwichtigsten 8 Bits Blau, und die wichtigsten 8 Bits entsprechen sind entweder null oder der Alphakanal.
Im Allgemeinen entspricht die Anordnung der Bytes in der Bitmap der Anordnung der Buchstaben B, G, R und A im Eigenschaftsnamen. Die Liste nicht beschreibbarer Formate beginnt mit mehreren Farbformaten, die nur 16 oder 24 Bits pro Pixel verwenden. Das Format „Bgr101010“ verwendet 32 Bits pro Pixel, jedoch 10 Bits für jede Grundfarbe. Wenn das Pixel als eine 32-Bit-Ganzzahl ohne Vorzeichen dargestellt wird, werden die unwichtigsten 10 Bits für Blau verwendet. Das Format „Cmyk32“ codiert Cyan, Magenta, Gelb und Schwarz, die bei der Druckausgabe verwendet werden.
Die Formate „Gray16“, „Rgb48“, „Rgba64“ und „Prgba64“ codieren eine 16-Bit-Grauschattierung und 16-Bit-Grundfarben. Wenn Sie mit Hardware und Anwendungen arbeiten, die eine derart hohe Farbgenauigkeit erfordern (z. B. bei der medizinischen Bildverarbeitung), freut es Sie sicher, dass Sie jetzt Bitmapdaten mit einer so hohen Auflösung speichern und anzeigen können. Es gibt jedoch keinen Grund, diese Formate in anderen Fällen zu verwenden. Die zusätzliche Farbgenauigkeit wird in Farbanzeigen mit 8 Bits pro Grundfarbe oder beim Speichern in Dateiformaten mit 8 Bits pro Grundfarbe ignoriert.
Die Liste der Pixelformate wird mit vier Formaten abgeschlossen, die Gleitkommawerte mit einfacher Genauigkeit verwenden, um Farbebenen und Transparenz darzustellen. Diese Formate basieren auf dem scRGB-Farbraum und einem Gammawert von 1 statt des üblichen sRGB-Farbraums und eines Gammawerts von 2,2. (Eine Erklärung finden Sie auf den Seiten 24 bis 25 meines Buchs „Applications = Code + Markup“.) Ein Gleitkommafarbwert von 0,0 entspricht einem Bytewert von 0x00 (Schwarz), und ein Gleitkommafarbwert von 1,0 entspricht einem Bytewert von 0xFF. Die Gleitkommafarbwerte können jedoch bei Anzeigegeräten mit einer breiteren Farbskala als Videoanzeigen den Wert 1 überschreiten.
PixelFormats-Errata
In Bezug auf einige Formate scheint es in der PixelFormats-Dokumentation Verwirrung zu geben. Die Formate „Gray16“, „Rgb48“, „Rgba64“ und „Prgba64“ basieren laut Dokumentation auf einem Gammawert von 1. Außerdem wird jedoch paradoxerweise angegeben, dass es sich (mit Ausnahme von „Gray16“) um sRGB-Formate handelt. Dies ist nicht der Fall. Nur die Float-Pixelformate verwenden das scRGB-Farbformat und einen Gammawert von 1.
Vielleicht möchten Sie eine verallgemeinerte Methode für die Trennung von Pixeln in ihre Farbkomponenten oder für den Aufbau von Pixeln aus Farbkomponenten. Die PixelFormat-Struktur enthält eine Eigenschaft namens „Mask“, die eine Sammlung von Objekten des Typs „PixelFormatChannelMask“ ist. Es gibt ein PixelFormatChannelMask-Objekt für jeden Farbkanal in der Reihenfolge Blau, Grün, Rot und Alpha.
Die PixelFormatChannelMask-Struktur definiert eine Mask-Eigenschaft, bei der es sich um eine Sammlung von Bytes handelt, deren Zahl gleich der Zahl der Bytes pro Pixel ist und die der Bytereihenfolge der Pixel entspricht. Beispielsweise gibt es für das Format „Bgr555“ drei PixelFormatChannelMask-Objekte, von denen jedes 2 Bytes groß ist. Für Blau sind die 2 Bytes 0x1F und 0x00, für Grün 0xE0 und 0x03 und für Rot sind es 0x00 und 0x7C. Wenn Sie diese Daten verwenden möchten, müssen Sie Ihre eigenen Faktoren zur Verschiebung von Bits ableiten.
Ich habe bereits erwähnt, dass Sie für die BitmapSource.Create-Methode jedes der PixelFormats-Elemente außer „PixelFormats.Default“ verwenden können, für den WriteableBitmap-Konstruktor jedoch nur die beschreibbaren Formate aus Abbildung 4. Wenn Sie sich die WriteableBitmap-Dokumentation näher ansehen, werden Sie einen alternativen Konstruktor entdecken, der ein WriteableBitmap-Objekt aus einem beliebigen BitmapSource-Objekt erstellt.
Sie können in der Tat zuerst ein BitmapSource-Objekt aus einem nicht beschreibbaren Format aus Abbildung 4 erstellen und dann eine „WriteableBitmap“ auf Basis dieser „BitmapSource“. Gehen Sie jedoch nicht davon aus, dass Sie die Einschränkung umgehen können: Jede Bitmap mit einem nicht beschreibbaren Format wird in ein Bgr32- oder Pbgra32-Format konvertiert, je nachdem, ob ein Alphakanal vorhanden ist oder nicht.
Sie können jede von Ihnen erstellte Bitmap in einer Datei in einem beliebigen unterstützten Dateiformat speichern, insbesondere BMP, GIF, PNG, JPEG, TIFF sowie Microsoft Windows Media Photo. Es ist jedoch möglich, dass die Bitmapdaten dabei in ein anderes Format umgewandelt werden. Beim Speichern als GIF-Datei wird die Bitmap beispielsweise immer zuerst in ein Indexed8-Format konvertiert. Beim Speichern als JPEG-Datei wird die Bitmap immer in entweder „Gray8“ oder „Bgr32“ umgewandelt. Gegenwärtig gibt es keine Kombination aus „PixelFormat“ und „BitmapEncoder“, die eine Datei ergibt, die pro Grundfarbe mehr als 8 Bits Daten enthält.
Premultiplied Alpha
Drei der statischen Eigenschaften der PixelFormats-Klasse beginnen mit dem Buchstaben P, der für Premultiplied Alpha (integriertes Alpha) steht. Dies ist ein Verfahren zur Verbesserung der Effizienz des Bitmap-Rendering für Pixel, die teilweise transparent sind. Es wird nur auf Bitmaps mit einem Alphakanal angewendet.
Angenommen, Sie haben einen „SolidColorBrush“ mit einer Farbe erstellt, die wie folgt berechnet wurde:
Color.FromArgb(128, 0, 0, 255)
Das ist ein blauer Pinsel mit einer Transparenz von 50 Prozent. Wenn dieser Pinsel gerendert wird, muss die Farbe mit der vorhandenen Farbe der Anzeigeoberfläche kombiniert werden. Beim Zeichnen auf einem schwarzen Hintergrund ist die resultierende RGB-Farbe (0, 0, 128). Auf einem weißen Hintergrund ist die resultierende Farbe (127, 127, 255). Wir berechnen einfach den gewichteten Durchschnitt.
Die tiefgestellten Zeichen in den folgenden Formeln geben das Ergebnis des Rendering eines teilweise transparenten Pixels auf einer vorhandenen Oberfläche an:
Rresult = [(255 – Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 – Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 – Apixel) * Bsurface + Apixel * Bpixel] / 255;
Diese Berechnung kann beschleunigt werden, wenn die Werte R, G und B des Pixels bereits mit dem Wert A multipliziert und durch 255 geteilt wurden. Die zweite Multiplikation in jeder Formel kann dann weggelassen werden. Angenommen, ein Pixel in einer Bgra32-Bitmap entspricht dem ARGB-Wert (192, 40, 60, 255). In einer Pbgra32-Bitmap entspräche dasselbe Pixel dem Wert (192, 30, 45, 192). Die RGB-Werte wurden bereits mit dem Alphawert 192 multipliziert und durch 255 geteilt.
Für alle Pixel in einer Pbgra32-Bitmap gilt, dass keiner der Werte R, G oder B größer sein sollte als der Wert A. Andernfalls erfolgt keine Vergrößerung. Die Werte sind an ein Maximum von 255 gebunden, Sie erhalten jedoch nicht die gewünschte Transparenzstufe.
WriteableBitmap-Anwendungen
Vielleicht verwenden Sie „WriteableBitmap“ in Anwendungen, in denen Sie einfache dynamische Grafiken (vielleicht ein Balkendiagramm) anzeigen müssen, und stellen fest, dass Sie eine Bitmap schneller aktualisieren können, als WPF die entsprechende Vektorgrafik zeichnen kann.
Am häufigsten wird „WriteableBitmap“ vermutlich bei der Bildverarbeitung in Echtzeit und nicht linearen Transformationen eingesetzt. Mit dem TwistedBitmap-Projekt können Sie eine beliebige Bitmap mit 8 Bits pro Pixel oder 32 Bits pro Pixel laden und ein Schieberegler-Steuerelement dazu verwenden, das Bild um die Mitte zu drehen (siehe Abbildung 8). Die besten Ergebnisse erzielen Sie mit kleineren Bildern.
Abbildung 8 Verdrehte Bitmap (Klicken Sie auf das Bild, um es zu vergrößern)
Das Programm verwendet „BitmapFrame.Create“ zum Laden einer Bitmap aus einer Datei und ruft dann „CopyPixels“ auf, um alle Pixelbits in ein Array namens „pixelsSrc“ zu kopieren. Der SliderOnValueChanged-Ereignishandler (siehe Abbildung 9) ist verantwortlich für das Transformieren der Pixel aus „pixelsSrc“ in ein Array namens „pixelsNew“, das zum Aufrufen von „WritePixels“ verwendet wird.
void SliderOnValueChanged(object sender,
                          RoutedPropertyChangedEventArgs<double> args)
{
    if (pixelsSrc == null)
        return;

    Slider slider = sender as Slider;
    int width = bitmap.PixelWidth;
    int height = bitmap.PixelHeight;
    int xCenter = width / 2;
    int yCenter = height / 2;
    int bytesPerPixel = bitmap.Format.BitsPerPixel / 8;

    for (int row = 0; row < bitmap.PixelHeight; row += 1)
    {
        for (int col = 0; col < bitmap.PixelWidth; col += 1)
        {
            // Calculate length of point to center and angle
            int xDelta = col - xCenter;
            int yDelta = row - yCenter;
            double distanceToCenter = Math.Sqrt(xDelta * xDelta +
                                                yDelta * yDelta);
            double angleClockwise = Math.Atan2(yDelta, xDelta);

            // Calculate angle of rotation for twisting effect 
            double xEllipse = xCenter * Math.Cos(angleClockwise);
            double yEllipse = yCenter * Math.Sin(angleClockwise);
            double radius = Math.Sqrt(xEllipse * xEllipse +
                                      yEllipse * yEllipse);
            double fraction = Math.Max(0, 1 - distanceToCenter / radius);
            double twist = fraction * Math.PI * slider.Value / 180;

            // Calculate the source pixel for each destination pixel
            int colSrc = (int) (xCenter + (col - xCenter) *
                                Math.Cos(twist)
                (row - yCenter) * Math.Sin(twist));
            int rowSrc = (int) (yCenter + (col - xCenter) *
                                Math.Sin(twist) 
                + (row - yCenter) * Math.Cos(twist));
            colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
            rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));

            // Calculate the indices
            int index = stride * row + bytesPerPixel * col;
            int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;

            // Transfer the pixels
            for (int i = 0; i < bytesPerPixel; i++)
                pixelsNew[index + i] = pixelsSrc[indexSrc + i];
        }
    }
    // Write out the array
    bitmap.WritePixels(rect, pixelsNew, stride, 0);
}
Beim Arbeiten mit Bitmaptransformationen ist es entscheidend, die Transformation umgekehrt auszuführen. Wenn Sie jedes Pixel in der ursprünglichen Bitmap untersuchen und bestimmen, wo es in der neuen Bitmap abgelegt werden soll, ist es sehr wahrscheinlich, dass verschiedene Pixel in der ursprünglichen Bitmap demselben Pixel in der neuen Bitmap zugeordnet werden. Das bedeutet, dass für einige Pixel in der neuen Bitmap kein Wert festgelegt wird! Das Bild hat „Löcher“.
Sie müssen stattdessen für jedes Pixel in der neuen Bitmap herausfinden, welches Pixel in der ursprünglichen Bitmap dieser bestimmten Zeile und Spalte zugeordnet ist. Mit dieser Herangehensweise wird sichergestellt, dass jedem Pixel in der neuen Bitmap ein errechneter Wert zugewiesen ist.

Senden Sie Fragen und Kommentare in englischer Sprache an mmnet30@microsoft.com.

Charles Petzold schreibt redaktionelle Beiträge für das MSDN Magazin. Sein neuestes Buch heißt The Annotated Turing: A Guided Tour through Alan Turing’s Historic Paper on Computability and the Turing Machine.

Page view tracker