Freigeben über


Zugreifen auf SkiaSharp-Bitmappixelbits

Wie Sie im Artikel "Speichern von SkiaSharp-Bitmaps in Dateien" gesehen haben, werden Bitmaps im Allgemeinen in Dateien in einem komprimierten Format gespeichert, z. B. JPEG oder PNG. In constrast wird eine im Arbeitsspeicher gespeicherte SkiaSharp-Bitmap nicht komprimiert. Sie wird als sequenzielle Datenreihe von Pixeln gespeichert. Dieses nicht komprimierte Format erleichtert die Übertragung von Bitmaps auf eine Anzeigeoberfläche.

Der von einer SkiaSharp-Bitmap belegte Speicherblock wird ganz einfach organisiert: Er beginnt mit der ersten Pixelzeile, von links nach rechts und setzt dann mit der zweiten Zeile fort. Bei vollfarbigen Bitmaps besteht jedes Pixel aus vier Bytes, was bedeutet, dass der gesamt für die Bitmap erforderliche Speicherplatz viermal das Produkt seiner Breite und Höhe ist.

In diesem Artikel wird beschrieben, wie eine Anwendung zugriff auf diese Pixel erhalten kann, entweder direkt durch Den Zugriff auf den Pixelspeicherblock der Bitmap oder indirekt. In einigen Fällen möchte ein Programm möglicherweise die Pixel eines Bilds analysieren und ein Histogramm einer Art erstellen. Häufig können Anwendungen eindeutige Bilder erstellen, indem sie die Pixel, aus denen die Bitmap besteht, algorithmisch erstellen:

Beispiele für Pixelbits

Die Techniken

SkiaSharp bietet mehrere Techniken für den Zugriff auf die Pixelbits einer Bitmap. Welche Sie sich entscheiden, ist in der Regel ein Kompromiss zwischen der Codierungsfreundlichkeit (was mit Standard Intensität und Leichtigkeit des Debuggens zusammenhängt) und der Leistung. In den meisten Fällen verwenden Sie eine der folgenden Methoden und Eigenschaften SKBitmap für den Zugriff auf die Bitmappixel:

  • Mit GetPixel den Methoden SetPixel können Sie die Farbe eines einzelnen Pixels abrufen oder festlegen.
  • Die Pixels Eigenschaft ruft ein Array von Pixelfarben für die gesamte Bitmap ab oder legt das Array der Farben fest.
  • GetPixels gibt die Adresse des pixelspeichers zurück, der von der Bitmap verwendet wird.
  • SetPixels ersetzt die Adresse des von der Bitmap verwendeten Pixelspeichers.

Sie können sich die ersten beiden Techniken als "High Level" und die zweiten beiden als "niedrig" vorstellen. Es gibt einige andere Methoden und Eigenschaften, die Sie verwenden können, aber dies sind die wertvollsten.

Damit Sie die Leistungsunterschiede zwischen diesen Techniken sehen können, enthält die Beispielanwendung eine Seite mit dem Namen Gradient Bitmap , die eine Bitmap mit Pixeln erstellt, die rote und blaue Schattierungen kombinieren, um einen Farbverlauf zu erstellen. Das Programm erstellt acht verschiedene Kopien dieser Bitmap, wobei alle verschiedene Techniken zum Festlegen der Bitmappixel verwendet werden. Jede dieser acht Bitmaps wird in einer separaten Methode erstellt, die auch eine kurze Textbeschreibung der Technik festlegt und die Zeit berechnet, die zum Festlegen aller Pixel erforderlich ist. Jede Methode durchläuft die Pixeleinstellungslogik 100 Mal, um eine bessere Schätzung der Leistung zu erzielen.

Die SetPixel-Methode

Wenn Sie nur mehrere einzelne Pixel festlegen oder abrufen müssen, sind die SetPixel Methoden GetPixel ideal. Für jede dieser beiden Methoden geben Sie die ganzzahlige Spalte und Zeile an. Unabhängig vom Pixelformat können Sie mit diesen beiden Methoden das Pixel als SKColor Wert abrufen oder festlegen:

bitmap.SetPixel(col, row, color);

SKColor color = bitmap.GetPixel(col, row);

Das col Argument muss von 0 bis 1 kleiner als die Width Eigenschaft der Bitmap liegen und row reicht von 0 bis 1 kleiner als die Height Eigenschaft.

Dies ist die Methode in Gradient Bitmap , die den Inhalt einer Bitmap mithilfe der SetPixel Methode festlegt. Die Bitmap beträgt 256 x 256 Pixel, und die for Schleifen sind hartcodiert mit dem Wertebereich:

public class GradientBitmapPage : ContentPage
{
    const int REPS = 100;

    Stopwatch stopwatch = new Stopwatch();
    ···
    SKBitmap FillBitmapSetPixel(out string description, out int milliseconds)
    {
        description = "SetPixel";
        SKBitmap bitmap = new SKBitmap(256, 256);

        stopwatch.Restart();

        for (int rep = 0; rep < REPS; rep++)
            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    bitmap.SetPixel(col, row, new SKColor((byte)col, 0, (byte)row));
                }

        milliseconds = (int)stopwatch.ElapsedMilliseconds;
        return bitmap;
    }
    ···
}

Der Farbsatz für jedes Pixel weist eine rote Komponente auf, die der Bitmapspalte entspricht, und eine blaue Komponente, die der Zeile entspricht. Die resultierende Bitmap ist schwarz oben links, rot oben rechts, blau unten links und Magenta unten rechts, mit Farbverläufen an anderer Stelle.

Die SetPixel Methode wird 65.536 Mal aufgerufen, und unabhängig davon, wie effizient diese Methode sein kann, empfiehlt es sich in der Regel nicht, dass viele API-Aufrufe ausgeführt werden, wenn eine Alternative verfügbar ist. Glücklicherweise gibt es mehrere Alternativen.

Die Pixels-Eigenschaft

SKBitmap definiert eine Pixels Eigenschaft, die ein Array von SKColor Werten für die gesamte Bitmap zurückgibt. Sie können auch Pixels ein Array von Farbwerten für die Bitmap festlegen:

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Die Pixel werden im Array beginnend mit der ersten Zeile, von links nach rechts, dann in der zweiten Zeile usw. angeordnet. Die Gesamtanzahl der Farben im Array entspricht dem Produkt der Bitmapbreite und -höhe.

Obwohl diese Eigenschaft effizient erscheint, denken Sie daran, dass die Pixel aus der Bitmap in das Array kopiert werden, und aus dem Array zurück in die Bitmap, und die Pixel werden von und in SKColor Werte konvertiert.

Dies ist die Methode in der GradientBitmapPage Klasse, die die Bitmap mithilfe der Pixels Eigenschaft festlegt. Die Methode weist ein SKColor Array der erforderlichen Größe zu, aber sie könnte die Pixels Eigenschaft zum Erstellen dieses Arrays verwendet haben:

SKBitmap FillBitmapPixelsProp(out string description, out int milliseconds)
{
    description = "Pixels property";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    SKColor[] pixels = new SKColor[256 * 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                pixels[256 * row + col] = new SKColor((byte)col, 0, (byte)row);
            }

    bitmap.Pixels = pixels;

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Beachten Sie, dass der Index des Arrays anhand row der pixels Variablen berechnet col werden muss. Die Zeile wird mit der Anzahl der Pixel in jeder Zeile multipliziert (in diesem Fall 256), und dann wird die Spalte hinzugefügt.

SKBitmap definiert auch eine ähnliche Bytes Eigenschaft, die ein Bytearray für die gesamte Bitmap zurückgibt, aber es ist für vollfarbige Bitmaps umständlicher.

Der GetPixels-Zeiger

Möglicherweise ist die leistungsstärkste Technik für den Zugriff auf die Bitmappixel GetPixelsnicht mit der GetPixel Methode oder der Pixels Eigenschaft zu verwechseln. Sie werden sofort einen Unterschied feststellen, da GetPixels er etwas nicht sehr häufig in der C#-Programmierung zurückgibt:

IntPtr pixelsAddr = bitmap.GetPixels();

Der .NET-Typ IntPtr stellt einen Zeiger dar. Es wird aufgerufen IntPtr , da es die Länge einer ganzen Zahl auf dem systemeigenen Prozessor des Computers ist, auf dem das Programm ausgeführt wird, im Allgemeinen 32 Bit oder 64 Bit länge. GetPixels Die IntPtr Rückgabe ist die Adresse des tatsächlichen Speicherblocks, den das Bitmapobjekt zum Speichern seiner Pixel verwendet.

Sie können den IntPtr Zeigertyp mit der ToPointer Methode in einen C#-Zeigertyp konvertieren. Die C#-Zeigersyntax ist identisch mit C und C++:

byte* ptr = (byte*)pixelsAddr.ToPointer();

Die ptr Variable ist vom Typ Bytezeiger. Mit dieser ptr Variablen können Sie auf die einzelnen Bytes des Arbeitsspeichers zugreifen, die zum Speichern der Bitmappixel verwendet werden. Sie verwenden Code wie diesen, um ein Byte aus diesem Speicher zu lesen oder ein Byte in den Speicher zu schreiben:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

In diesem Kontext ist das Sternchen der C# -Dereferenzierungsoperator und wird verwendet, um auf den Inhalt des Speichers zu verweisen, auf ptrden verwiesen wird. Zeigt zunächst ptr auf das erste Byte des ersten Pixels der ersten Zeile der Bitmap, sie kann jedoch arithmetisch für die ptr Variable ausgeführt werden, um es an andere Positionen innerhalb der Bitmap zu verschieben.

Ein Nachteil ist, dass Sie diese ptr Variable nur in einem Codeblock verwenden können, der mit dem unsafe Schlüsselwort (keyword) gekennzeichnet ist. Darüber hinaus muss die Assembly als unsichere Blöcke gekennzeichnet werden. Dies geschieht in den Eigenschaften des Projekts.

Die Verwendung von Zeigern in C# ist sehr leistungsfähig, aber auch sehr gefährlich. Sie müssen darauf achten, dass Sie nicht über das verweisen, was der Zeiger referenzieren soll, nicht auf den Speicher zugreifen. Aus diesem Grund ist die Zeigerverwendung mit dem Wort "unsicher" verknüpft.

Dies ist die Methode in der GradientBitmapPage Klasse, die die GetPixels Methode verwendet. Beachten Sie den unsafe Block, der den gesamten Code mit dem Bytezeiger umfasst:

SKBitmap FillBitmapBytePtr(out string description, out int milliseconds)
{
    description = "GetPixels byte ptr";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    IntPtr pixelsAddr = bitmap.GetPixels();

    unsafe
    {
        for (int rep = 0; rep < REPS; rep++)
        {
            byte* ptr = (byte*)pixelsAddr.ToPointer();

            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    *ptr++ = (byte)(col);   // red
                    *ptr++ = 0;             // green
                    *ptr++ = (byte)(row);   // blue
                    *ptr++ = 0xFF;          // alpha
                }
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Wenn die ptr Variable zum ersten Mal aus der ToPointer Methode abgerufen wird, verweist sie auf das erste Byte des Pixels ganz links der ersten Zeile der Bitmap. Die for Schleifen für row und col werden so eingerichtet, dass ptr nach jedem Byte jedes Pixels mit dem ++ Operator erhöht werden können. Für die anderen 99 Schleifen durch die Pixel muss der ptr Wert auf den Anfang der Bitmap zurückgesetzt werden.

Jedes Pixel ist vier Bytes Arbeitsspeicher, sodass jedes Byte separat festgelegt werden muss. Im folgenden Code wird davon ausgegangen, dass sich die Bytes in der Reihenfolge rot, grün, blau und alpha befinden, die mit dem SKColorType.Rgba8888 Farbtyp konsistent ist. Möglicherweise erinnern Sie sich daran, dass dies der Standardfarbtyp für iOS und Android ist, aber nicht für die Universelle Windows-Plattform. Standardmäßig erstellt die UWP Bitmaps mit dem SKColorType.Bgra8888 Farbtyp. Aus diesem Grund erwarten Sie, dass auf dieser Plattform einige verschiedene Ergebnisse angezeigt werden!

Es ist möglich, den von ToPointer einem Zeiger zurückgegebenen Wert anstelle eines Zeigers in einen byte uint Zeiger zu umwandeln. Dadurch kann in einer Anweisung auf ein gesamtes Pixel zugegriffen werden. Durch Anwenden des ++ Operators auf diesen Zeiger wird der Operator um vier Byte erhöht, um auf das nächste Pixel zu zeigen:

public class GradientBitmapPage : ContentPage
{
    ···
    SKBitmap FillBitmapUintPtr(out string description, out int milliseconds)
    {
        description = "GetPixels uint ptr";
        SKBitmap bitmap = new SKBitmap(256, 256);

        stopwatch.Restart();

        IntPtr pixelsAddr = bitmap.GetPixels();

        unsafe
        {
            for (int rep = 0; rep < REPS; rep++)
            {
                uint* ptr = (uint*)pixelsAddr.ToPointer();

                for (int row = 0; row < 256; row++)
                    for (int col = 0; col < 256; col++)
                    {
                        *ptr++ = MakePixel((byte)col, 0, (byte)row, 0xFF);
                    }
            }
        }

        milliseconds = (int)stopwatch.ElapsedMilliseconds;
        return bitmap;
    }
    ···
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    uint MakePixel(byte red, byte green, byte blue, byte alpha) =>
            (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
    ···
}

Das Pixel wird mit der Methode festgelegt, mit der MakePixel ein ganzzahliges Pixel aus roten, grünen, blauen und Alphakomponenten erstellt wird. Denken Sie daran, dass das SKColorType.Rgba8888 Format eine Pixelbyte-Sortierung wie folgt aufweist:

RR GG BB AA

Die ganze Zahl, die diesen Bytes entspricht, lautet jedoch:

AABBGGRR

Das am wenigsten signifikante Byte der ganzen Zahl wird zuerst in Übereinstimmung mit der little-endischen Architektur gespeichert. Diese MakePixel Methode funktioniert für Bitmaps mit dem Bgra8888 Farbtyp nicht ordnungsgemäß.

Die MakePixel Methode wird mit der MethodImplOptions.AggressiveInlining Option gekennzeichnet, den Compiler zu ermutigen, dies nicht zu einer separaten Methode zu machen, sondern stattdessen den Code zu kompilieren, in dem die Methode aufgerufen wird. Dies sollte die Leistung verbessern.

Interessanterweise definiert die SKColor Struktur eine explizite Konvertierung von SKColor einer nicht signierten ganzzahligen Zahl, was bedeutet, dass ein SKColor Wert erstellt werden kann, und eine Konvertierung, die uint anstelle von MakePixel:

SKBitmap FillBitmapUintPtrColor(out string description, out int milliseconds)
{
    description = "GetPixels SKColor";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    IntPtr pixelsAddr = bitmap.GetPixels();

    unsafe
    {
        for (int rep = 0; rep < REPS; rep++)
        {
            uint* ptr = (uint*)pixelsAddr.ToPointer();

            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    *ptr++ = (uint)new SKColor((byte)col, 0, (byte)row);
                }
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Die einzige Frage ist: Ist das ganzzahlige Format des SKColor Werts in der Reihenfolge des SKColorType.Rgba8888 Farbtyps oder des SKColorType.Bgra8888 Farbtyps, oder ist es etwas anderes? Die Antwort auf diese Frage wird in Kürze offengelegt.

Die SetPixels-Methode

SKBitmap definiert außerdem eine Methode namens SetPixels, die Sie wie folgt aufrufen:

bitmap.SetPixels(intPtr);

Erinnern Sie sich daran, dass GetPixels ein IntPtr Verweis auf den speicherblock, der von der Bitmap zum Speichern der Pixel verwendet wird. Der SetPixels Aufruf ersetzt diesen Speicherblock durch den Speicherblock, auf den durch das IntPtr angegebene SetPixels Argument verwiesen wird. Die Bitmap gibt dann den Speicherblock frei, den er zuvor verwendet hat. Beim nächsten Aufruf GetPixels wird der speicherblock festgelegt mit SetPixels.

Zunächst scheint es so, als ob SetPixels Sie nicht mehr Leistung und Leistung bietet, als GetPixels wenn Sie weniger bequem sind. Mit GetPixels dem Abrufen des Bitmapspeicherblocks und des Zugriffs darauf. Wenn SetPixels Sie Arbeitsspeicher zuweisen und darauf zugreifen, und legen Sie diese dann als Bitmapspeicherblock fest.

Die Verwendung SetPixels bietet jedoch einen eindeutigen syntaktischen Vorteil: Sie ermöglicht es Ihnen, mithilfe eines Arrays auf die Bitmappixelbits zuzugreifen. Hier ist die Methode, in GradientBitmapPage der diese Technik veranschaulicht wird. Die Methode definiert zunächst ein mehrdimensionales Bytearray, das den Bytes der Bitmappixel entspricht. Die erste Dimension ist die Zeile, die zweite Dimension ist die Spalte, und die dritte Dimension korreliert mit den vier Komponenten jedes Pixels:

SKBitmap FillBitmapByteBuffer(out string description, out int milliseconds)
{
    description = "SetPixels byte buffer";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    byte[,,] buffer = new byte[256, 256, 4];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col, 0] = (byte)col;   // red
                buffer[row, col, 1] = 0;           // green
                buffer[row, col, 2] = (byte)row;   // blue
                buffer[row, col, 3] = 0xFF;        // alpha
            }

    unsafe
    {
        fixed (byte* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Nachdem das Array mit Pixeln gefüllt wurde, wird ein Block und eine fixed Anweisung verwendet, unsafe um einen Bytezeiger abzurufen, der auf dieses Array zeigt. Dieser Bytezeiger kann dann in einen IntPtr Zulauf umwandeln.SetPixels

Das von Ihnen erstellte Array muss kein Bytearray sein. Es kann sich um ein ganzzahliges Array mit nur zwei Dimensionen für die Zeile und Spalte handeln:

SKBitmap FillBitmapUintBuffer(out string description, out int milliseconds)
{
    description = "SetPixels uint buffer";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    uint[,] buffer = new uint[256, 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col] = MakePixel((byte)col, 0, (byte)row, 0xFF);
            }

    unsafe
    {
        fixed (uint* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Die MakePixel Methode wird erneut verwendet, um die Farbkomponenten in einem 32-Bit-Pixel zu kombinieren.

Nur für Vollständigkeit ist hier derselbe Code, aber mit einem SKColor Wert, der in eine nicht signierte ganze Zahl umgewandelt wird:

SKBitmap FillBitmapUintBufferColor(out string description, out int milliseconds)
{
    description = "SetPixels SKColor";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    uint[,] buffer = new uint[256, 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col] = (uint)new SKColor((byte)col, 0, (byte)row);
            }

    unsafe
    {
        fixed (uint* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Vergleichen der Techniken

Der Konstruktor der Farbverlaufsseite ruft alle acht oben gezeigten Methoden auf und speichert die Ergebnisse:

public class GradientBitmapPage : ContentPage
{
    ···
    string[] descriptions = new string[8];
    SKBitmap[] bitmaps = new SKBitmap[8];
    int[] elapsedTimes = new int[8];

    SKCanvasView canvasView;

    public GradientBitmapPage ()
    {
        Title = "Gradient Bitmap";

        bitmaps[0] = FillBitmapSetPixel(out descriptions[0], out elapsedTimes[0]);
        bitmaps[1] = FillBitmapPixelsProp(out descriptions[1], out elapsedTimes[1]);
        bitmaps[2] = FillBitmapBytePtr(out descriptions[2], out elapsedTimes[2]);
        bitmaps[4] = FillBitmapUintPtr(out descriptions[4], out elapsedTimes[4]);
        bitmaps[6] = FillBitmapUintPtrColor(out descriptions[6], out elapsedTimes[6]);
        bitmaps[3] = FillBitmapByteBuffer(out descriptions[3], out elapsedTimes[3]);
        bitmaps[5] = FillBitmapUintBuffer(out descriptions[5], out elapsedTimes[5]);
        bitmaps[7] = FillBitmapUintBufferColor(out descriptions[7], out elapsedTimes[7]);

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ···
}

Der Konstruktor schließt mit dem Erstellen einer SKCanvasView Bitmap zum Anzeigen der resultierenden Bitmaps. Der PaintSurface Handler teilt seine Oberfläche in acht Rechtecke und Aufrufe Display auf, um die einzelnen Anzuzeigenden anzuzeigen:

public class GradientBitmapPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        int width = info.Width;
        int height = info.Height;

        canvas.Clear();

        Display(canvas, 0, new SKRect(0, 0, width / 2, height / 4));
        Display(canvas, 1, new SKRect(width / 2, 0, width, height / 4));
        Display(canvas, 2, new SKRect(0, height / 4, width / 2, 2 * height / 4));
        Display(canvas, 3, new SKRect(width / 2, height / 4, width, 2 * height / 4));
        Display(canvas, 4, new SKRect(0, 2 * height / 4, width / 2, 3 * height / 4));
        Display(canvas, 5, new SKRect(width / 2, 2 * height / 4, width, 3 * height / 4));
        Display(canvas, 6, new SKRect(0, 3 * height / 4, width / 2, height));
        Display(canvas, 7, new SKRect(width / 2, 3 * height / 4, width, height));
    }

    void Display(SKCanvas canvas, int index, SKRect rect)
    {
        string text = String.Format("{0}: {1:F1} msec", descriptions[index],
                                    (double)elapsedTimes[index] / REPS);

        SKRect bounds = new SKRect();

        using (SKPaint textPaint = new SKPaint())
        {
            textPaint.TextSize = (float)(12 * canvasView.CanvasSize.Width / canvasView.Width);
            textPaint.TextAlign = SKTextAlign.Center;
            textPaint.MeasureText("Tly", ref bounds);

            canvas.DrawText(text, new SKPoint(rect.MidX, rect.Bottom - bounds.Bottom), textPaint);
            rect.Bottom -= bounds.Height;
            canvas.DrawBitmap(bitmaps[index], rect, BitmapStretch.Uniform);
        }
    }
}

Damit der Compiler den Code optimieren kann, wurde diese Seite im Releasemodus ausgeführt. Hier sehen Sie diese Seite, die auf einem i Telefon 8 Simulator auf einem MacBook Pro, einem Nexus 5 Android-Smartphone und Surface Pro 3 unter Windows 10 ausgeführt wird. Vermeiden Sie aufgrund der Hardwareunterschiede den Vergleich der Leistungszeiten zwischen den Geräten, sondern sehen Sie sich stattdessen die relativen Zeiten auf jedem Gerät an:

Farbverlaufsbitmap

Hier ist eine Tabelle, die die Ausführungszeiten in Millisekunden konsolidiert:

API Datentyp iOS Android UWP
SetPixel 3,17 10.77 3.49
Pixel 0,32 1.23 0,07
GetPixels Byte 0.09 0.24 0,10
uint 0.06 0.26 0.05
SKColor 0,29 0.99 0,07
SetPixels Byte 1,33 6,78 0,11
uint 0.14 0.69 0.06
SKColor 0,35 1,93 0,10

Wie erwartet, ist das Aufrufen von SetPixel 65.536 Mal die am wenigsten effiziente Methode zum Festlegen der Pixel einer Bitmap. Das Ausfüllen eines SKColor Arrays und das Festlegen der Pixels Eigenschaft ist viel besser und vergleicht sogar günstig mit einigen der GetPixels Techniken SetPixels . Das Arbeiten mit uint Pixelwerten ist im Allgemeinen schneller als das Festlegen separater byte Komponenten und das Konvertieren des SKColor Werts in eine nicht signierte ganze Zahl erhöht den Prozess.

Es ist auch interessant, die verschiedenen Farbverläufe zu vergleichen: Die obersten Zeilen jeder Plattform sind identisch und zeigen den Farbverlauf wie beabsichtigt an. Dies bedeutet, dass die SetPixel Methode und die Pixels Eigenschaft Pixel unabhängig vom zugrunde liegenden Pixelformat korrekt aus Farben erstellen.

Die nächsten beiden Zeilen der iOS- und Android-Screenshots sind ebenfalls identisch, wodurch bestätigt wird, dass die kleine MakePixel Methode für das Standardpixelformat Rgba8888 für diese Plattformen korrekt definiert ist.

Die untere Zeile der iOS- und Android-Screenshots ist rückwärts, was angibt, dass die nicht signierte ganze Zahl, die durch Umwandlung eines SKColor Werts abgerufen wird, in der Form ist:

AARRGGBB

Die Bytes sind in der Reihenfolge:

BB GG RR AA

Dies ist die Bgra8888 Sortierung anstelle der Rgba8888 Sortierung. Das Brga8888 Format ist die Standardeinstellung für die universelle Windows-Plattform, weshalb die Farbverläufe in der letzten Zeile dieses Screenshots mit der ersten Zeile identisch sind. Die mittleren beiden Zeilen sind jedoch falsch, da der Code, der diese Bitmaps erstellt, eine Rgba8888 Sortierung angenommen hat.

Wenn Sie denselben Code für den Zugriff auf Pixelbits auf jeder Plattform verwenden möchten, können Sie explizit eine SKBitmap Verwendung des Formats oder Bgra8888 des Rgba8888 Formats erstellen. Wenn Sie Werte in Bitmappixel umwandeln SKColor möchten, verwenden Sie Bgra8888.

Zufälliger Zugriff auf Pixel

Die FillBitmapBytePtr Methoden und FillBitmapUintPtr Methoden auf der Seite "Gradient Bitmap " profitierten von for Schleifen, die so konzipiert sind, dass die Bitmap sequenziell gefüllt wird, von der obersten Zeile bis zur unteren Zeile und in jeder Zeile von links nach rechts. Das Pixel könnte mit derselben Anweisung festgelegt werden, die den Zeiger erhöht hat.

Manchmal ist es notwendig, zufällig auf die Pixel zuzugreifen, anstatt sequenziell auf die Pixel zuzugreifen. Wenn Sie den GetPixels Ansatz verwenden, müssen Sie Zeiger basierend auf der Zeile und Spalte berechnen. Dies wird auf der Rainbow Sine-Seite veranschaulicht, die eine Bitmap mit einem Regenbogen in Form eines Zyklus einer Sinuskurve erstellt.

Die Farben des Regenbogens sind am einfachsten mit dem HSL-Farbmodell (Farbton, Sättigung, Leuchtdichte) zu erstellen. Die SKColor.FromHsl Methode erstellt einen SKColor Wert mit Farbtonwerten, die zwischen 0 und 360 liegen (z. B. die Winkel eines Kreises, aber von Rot, durch Grün und Blau und zurück zu Rot), sowie Sättigungs- und Leuchtdichtewerte zwischen 0 und 100. Für die Farben eines Regenbogens sollte die Sättigung auf maximal 100 und die Leuchtdichte auf einen Mittelpunkt von 50 festgelegt werden.

Rainbow Sine erstellt dieses Bild, indem die Zeilen der Bitmap durchlaufen und dann durch 360 Farbtonwerte durchlaufen werden. Aus jedem Farbtonwert berechnet sie eine Bitmapspalte, die auch auf einem Sinuswert basiert:

public class RainbowSinePage : ContentPage
{
    SKBitmap bitmap;

    public RainbowSinePage()
    {
        Title = "Rainbow Sine";

        bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);

        unsafe
        {
            // Pointer to first pixel of bitmap
            uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();

            // Loop through the rows
            for (int row = 0; row < bitmap.Height; row++)
            {
                // Calculate the sine curve angle and the sine value
                double angle = 2 * Math.PI * row / bitmap.Height;
                double sine = Math.Sin(angle);

                // Loop through the hues
                for (int hue = 0; hue < 360; hue++)
                {
                    // Calculate the column
                    int col = (int)(360 + 360 * sine + hue);

                    // Calculate the address
                    uint* ptr = basePtr + bitmap.Width * row + col;

                    // Store the color value
                    *ptr = (uint)SKColor.FromHsl(hue, 100, 50);
                }
            }
        }

        // Create the SKCanvasView
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect);
    }
}

Beachten Sie, dass der Konstruktor die Bitmap basierend auf dem SKColorType.Bgra8888 Format erstellt:

bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);

Auf diese Weise kann das Programm die Konvertierung von SKColor Werten uint in Pixel ohne Sorge verwenden. Obwohl es in diesem bestimmten Programm keine Rolle spielt, sollten Sie bei verwendung der Konvertierung zum Festlegen von SKColor Pixeln auch angeben, da SKColor die Farbkomponenten nicht vorab durch den Alphawert angegeben SKAlphaType.Unpremul werden.

Der Konstruktor verwendet dann die GetPixels Methode, um einen Zeiger auf das erste Pixel der Bitmap abzurufen:

uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();

Für eine bestimmte Zeile und Spalte muss ein Offsetwert hinzugefügt basePtrwerden. Dieser Offset ist die Zeilenzeit der Bitmapbreite sowie die Spalte:

uint* ptr = basePtr + bitmap.Width * row + col;

Der SKColor Wert wird mithilfe dieses Zeigers im Arbeitsspeicher gespeichert:

*ptr = (uint)SKColor.FromHsl(hue, 100, 50);

PaintSurface Im Handler der SKCanvasViewBitmap wird gestreckt, um den Anzeigebereich auszufüllen:

Regenbogensine

Von einer Bitmap zur anderen

Sehr viele Bildverarbeitungsaufgaben umfassen das Ändern von Pixeln, da sie von einer Bitmap in eine andere übertragen werden. Diese Technik wird auf der Seite " Farbanpassung " veranschaulicht. Die Seite lädt eine der Bitmapressourcen und ermöglicht es Ihnen dann, das Bild mithilfe von drei Slider Ansichten zu ändern:

Farbanpassung

Für jede Pixelfarbe fügt der Erste Slider einen Wert von 0 bis 360 zum Farbton hinzu, verwendet dann jedoch den Modulooperator, um das Ergebnis zwischen 0 und 360 zu halten, wodurch die Farben entlang des Spektrums effektiv verschoben werden (wie der UWP-Screenshot veranschaulicht). Mit der zweiten Slider Option können Sie einen multiplizierten Faktor zwischen 0,5 und 2 auswählen, der auf die Sättigung angewendet werden soll, und der dritte Slider führt dieselbe Funktion für die Leuchtdichte aus, wie im Android-Screenshot gezeigt.

Das Programm Standard enthält zwei Bitmaps, die ursprüngliche Quellbitmap namens srcBitmap und die angepasste Zielbitmap namens dstBitmap. Jedes Mal, wenn ein Slider Objekt verschoben wird, berechnet das Programm alle neuen Pixel in dstBitmap. Natürlich experimentieren Benutzer, indem Sie die Slider Ansichten sehr schnell verschieben, damit Sie die beste Leistung wünschen, die Sie verwalten können. Dies umfasst die Methode für die GetPixels Quell- und Zielbitmaps.

Die Seite "Farbanpassung " steuert nicht das Farbformat der Quell- und Zielbitmaps. Stattdessen enthält sie leicht unterschiedliche Logik für SKColorType.Rgba8888 und SKColorType.Bgra8888 Formate. Die Quelle und das Ziel können unterschiedliche Formate sein, und das Programm funktioniert weiterhin.

Hier sehen Sie das Programm mit Ausnahme der entscheidenden TransferPixels Methode, mit der die Pixel an das Ziel übertragen werden. Der Konstruktor legt dstBitmap gleich .srcBitmap Der PaintSurface Handler zeigt folgendes an dstBitmap:

public partial class ColorAdjustmentPage : ContentPage
{
    SKBitmap srcBitmap =
        BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    SKBitmap dstBitmap;

    public ColorAdjustmentPage()
    {
        InitializeComponent();

        dstBitmap = new SKBitmap(srcBitmap.Width, srcBitmap.Height);
        OnSliderValueChanged(null, null);
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        float hueAdjust = (float)hueSlider.Value;
        hueLabel.Text = $"Hue Adjustment: {hueAdjust:F0}";

        float saturationAdjust = (float)Math.Pow(2, saturationSlider.Value);
        saturationLabel.Text = $"Saturation Adjustment: {saturationAdjust:F2}";

        float luminosityAdjust = (float)Math.Pow(2, luminositySlider.Value);
        luminosityLabel.Text = $"Luminosity Adjustment: {luminosityAdjust:F2}";

        TransferPixels(hueAdjust, saturationAdjust, luminosityAdjust);
        canvasView.InvalidateSurface();
    }
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(dstBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

Der ValueChanged Handler für die Slider Ansichten berechnet die Anpassungswerte und Aufrufe TransferPixels.

Die gesamte TransferPixels Methode ist als unsafegekennzeichnet. Sie beginnt mit dem Abrufen von Bytezeigern auf die Pixelbits beider Bitmaps und durchläuft dann alle Zeilen und Spalten. Aus der Quellbitmap ruft die Methode vier Byte für jedes Pixel ab. Dies kann entweder in der Reihenfolge oder Bgra8888 in der Rgba8888 reihenfolge sein. Die Überprüfung auf den Farbtyp ermöglicht die Erstellung eines SKColor Werts. Die HSL-Komponenten werden dann extrahiert, angepasst und verwendet, um den SKColor Wert neu zu erstellen. Je nachdem, ob die Zielbitmap vorhanden ist Rgba8888 oder Bgra8888ob die Bytes im Zielbitmp gespeichert werden:

public partial class ColorAdjustmentPage : ContentPage
{
    ···
    unsafe void TransferPixels(float hueAdjust, float saturationAdjust, float luminosityAdjust)
    {
        byte* srcPtr = (byte*)srcBitmap.GetPixels().ToPointer();
        byte* dstPtr = (byte*)dstBitmap.GetPixels().ToPointer();

        int width = srcBitmap.Width;       // same for both bitmaps
        int height = srcBitmap.Height;

        SKColorType typeOrg = srcBitmap.ColorType;
        SKColorType typeAdj = dstBitmap.ColorType;

        for (int row = 0; row < height; row++)
        {
            for (int col = 0; col < width; col++)
            {
                // Get color from original bitmap
                byte byte1 = *srcPtr++;         // red or blue
                byte byte2 = *srcPtr++;         // green
                byte byte3 = *srcPtr++;         // blue or red
                byte byte4 = *srcPtr++;         // alpha

                SKColor color = new SKColor();

                if (typeOrg == SKColorType.Rgba8888)
                {
                    color = new SKColor(byte1, byte2, byte3, byte4);
                }
                else if (typeOrg == SKColorType.Bgra8888)
                {
                    color = new SKColor(byte3, byte2, byte1, byte4);
                }

                // Get HSL components
                color.ToHsl(out float hue, out float saturation, out float luminosity);

                // Adjust HSL components based on adjustments
                hue = (hue + hueAdjust) % 360;
                saturation = Math.Max(0, Math.Min(100, saturationAdjust * saturation));
                luminosity = Math.Max(0, Math.Min(100, luminosityAdjust * luminosity));

                // Recreate color from HSL components
                color = SKColor.FromHsl(hue, saturation, luminosity);

                // Store the bytes in the adjusted bitmap
                if (typeAdj == SKColorType.Rgba8888)
                {
                    *dstPtr++ = color.Red;
                    *dstPtr++ = color.Green;
                    *dstPtr++ = color.Blue;
                    *dstPtr++ = color.Alpha;
                }
                else if (typeAdj == SKColorType.Bgra8888)
                {
                    *dstPtr++ = color.Blue;
                    *dstPtr++ = color.Green;
                    *dstPtr++ = color.Red;
                    *dstPtr++ = color.Alpha;
                }
            }
        }
    }
    ···
}

Es ist wahrscheinlich, dass die Leistung dieser Methode noch verbessert werden könnte, indem separate Methoden für die verschiedenen Kombinationen von Farbtypen der Quell- und Zielbitmaps erstellt werden, und vermeiden Sie die Überprüfung des Typs für jedes Pixel. Eine weitere Option besteht darin, mehrere for Schleifen für die col Variable basierend auf dem Farbtyp zu verwenden.

Posterization

Ein weiterer gängiger Auftrag für den Zugriff auf Pixelbits ist die Posterisierung. Die Zahl, wenn farbencodiert in den Pixeln einer Bitmap reduziert werden, sodass das Ergebnis einem handgezeichneten Poster mit einer begrenzten Farbpalette ähnelt.

Die Posterize-Seite führt diesen Prozess auf einem der Affenbilder aus:

public class PosterizePage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    public PosterizePage()
    {
        Title = "Posterize";

        unsafe
        {
            uint* ptr = (uint*)bitmap.GetPixels().ToPointer();
            int pixelCount = bitmap.Width * bitmap.Height;

            for (int i = 0; i < pixelCount; i++)
            {
                *ptr++ &= 0xE0E0E0FF;
            }
        }

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform;
    }
}

Der Code im Konstruktor greift auf jedes Pixel zu, führt einen bitweisen AND-Vorgang mit dem Wert 0xE0E0E0FF aus und speichert das Ergebnis dann wieder in der Bitmap. Die Werte 0xE0E0E0FF behalten die hohen 3 Bits jeder Farbkomponente bei und legen die unteren 5 Bits auf 0 fest. Anstatt 24 oder 16.777.216 Farben, wird die Bitmap auf 29 oder 512 Farben reduziert:

Screenshot zeigt ein posterize image of a toy monkey on two mobile devices and a desktop window.