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:
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 MethodenSetPixel
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 GetPixels
nicht 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 ptr
den 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:
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 basePtr
werden. 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 SKCanvasView
Bitmap wird gestreckt, um den Anzeigebereich auszufüllen:
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:
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 unsafe
gekennzeichnet. 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 Bgra8888
ob 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: