SkiaSharp bit eşlem piksel bitlerine erişme

SkiaSharp bit eşlemlerini dosyalara kaydetme makalesinde gördüğünüz gibi bit eşlemler genellikle dosyalarda JPEG veya PNG gibi sıkıştırılmış bir biçimde depolanır. Constrast'ta, bellekte depolanan bir SkiaSharp bit eşlemi sıkıştırılmaz. Sıralı bir piksel serisi olarak depolanır. Bu sıkıştırılmamış biçim bit eşlemlerin bir görüntü yüzeyine aktarılmasını kolaylaştırır.

Bir SkiaSharp bit eşleminin kapladığı bellek bloğu çok basit bir şekilde düzenlenir: Soldan sağa ilk piksel satırıyla başlar ve ikinci satırla devam eder. Tam renkli bit eşlemler için her piksel dört bayttır; bu da bit eşlem için gereken toplam bellek alanının genişliğinin ve yüksekliğinin dört katı olduğu anlamına gelir.

Bu makalede, bir uygulamanın bit eşlemin piksel bellek bloğuna doğrudan erişerek veya dolaylı olarak bu piksellere nasıl erişebileceği açıklanmaktadır. Bazı durumlarda, bir program görüntünün piksellerini analiz etmek ve bir tür histogram oluşturmak isteyebilir. Daha yaygın olarak uygulamalar, bit eşlemi oluşturan pikselleri algoritmalı olarak oluşturarak benzersiz görüntüler oluşturabilir:

Piksel Bit örnekleri

Teknikler

SkiaSharp, bit eşlem piksel bitlerine erişmek için çeşitli teknikler sağlar. Hangisini seçtiğiniz genellikle kodlama kolaylığı (bakım ve hata ayıklama kolaylığı ile ilgilidir) ile performans arasında bir uzlaşmadır. Çoğu durumda bit eşlem piksellerine erişmek için aşağıdaki yöntemlerden ve özelliklerinden SKBitmap birini kullanacaksınız:

  • GetPixel ve SetPixel yöntemleri, tek bir pikselin rengini almanıza veya ayarlamanıza olanak sağlar.
  • özelliği tüm Pixels bit eşlem için bir piksel renkleri dizisi alır veya renk dizisini ayarlar.
  • GetPixels bit eşlem tarafından kullanılan piksel belleğinin adresini döndürür.
  • SetPixels bit eşlem tarafından kullanılan piksel belleğinin adresini değiştirir.

İlk iki tekniği "yüksek düzey" ve ikinci ikisini de "düşük düzey" olarak düşünebilirsiniz. Kullanabileceğiniz başka yöntemler ve özellikler de vardır, ancak bunlar en değerli olanlardır.

Bu teknikler arasındaki performans farklarını görmenize olanak sağlamak için örnek uygulama, gradyan oluşturmak için kırmızı ve mavi tonlarını birleştiren piksellerle bit eşlem oluşturan Gradyan Bit Eşlem adlı bir sayfa içerir. Program, bit eşlem piksellerini ayarlamak için farklı teknikler kullanan bu bit eşlemin sekiz farklı kopyasını oluşturur. Bu sekiz bit eşlemin her biri, tekniğin kısa bir metin açıklamasını ayarlayan ve tüm pikselleri ayarlamak için gereken süreyi hesaplayan ayrı bir yöntemde oluşturulur. Her yöntem, performansın daha iyi bir tahminini elde etmek için piksel ayarlama mantığını 100 kez döngüye alır.

SetPixel yöntemi

Yalnızca birkaç piksel SetPixel ayarlamanız veya almanız gerekiyorsa ve GetPixel yöntemleri idealdir. Bu iki yöntemin her biri için tamsayı sütununu ve satırını belirtirsiniz. Piksel biçiminden bağımsız olarak, bu iki yöntem pikseli bir değer olarak SKColor almanıza veya ayarlamanıza olanak sağlar:

bitmap.SetPixel(col, row, color);

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

col Bağımsız değişken bit eşlem özelliğinden 0 ile bir küçük Width arasında ve row 0 ile özelliğinden bir küçük Height arasında olmalıdır.

Gradyan Bit Eşlem'de yöntemini kullanarak bit eşlem içeriğini ayarlayan yöntem aşağıdadırSetPixel. Bit eşlem 256 x 256 pikseldir ve for döngüler değer aralığıyla sabit kodlanır:

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;
    }
    ···
}

Her piksel için renk kümesi bit eşlem sütununa eşit kırmızı bir bileşene ve satıra eşit mavi bir bileşene sahiptir. Sonuçta elde edilen bit eşlem sol üstte siyah, sağ üstte kırmızı, sol altta mavi ve diğer yerlerde gradyanlarla birlikte sağ altta eflatundur.

SetPixel yöntemi 65.536 kez çağrılır ve bu yöntemin ne kadar verimli olabileceğinden bağımsız olarak, bir alternatif varsa bu kadar çok API çağrısı yapmak genellikle iyi bir fikir değildir. Neyse ki, birkaç alternatif vardır.

Piksel özelliği

SKBitmapPixels tüm bit eşlem için bir değer dizisi SKColor döndüren bir özellik tanımlar. Bit eşlem için renk değerleri dizisi ayarlamak için de kullanabilirsiniz Pixels :

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Pikseller dizide ilk satırdan başlayarak soldan sağa, ikinci satırdan başlayarak vb. düzenlenir. Dizideki toplam renk sayısı, bit eşlem genişliği ve yüksekliğinin çarpımlarına eşittir.

Bu özellik verimli gibi görünse de piksellerin bit eşlemden diziye, diziden bit eşlem içine kopyalandığını ve piksellerin de değerlerden ve değerlerine SKColor dönüştürüldüğünü unutmayın.

Burada, özelliğini kullanarak bit eşlemi ayarlayan sınıfındaki yöntemi GradientBitmapPage gösterilmiştir Pixels . yöntemi gerekli boyutta bir SKColor dizi ayırır, ancak bu diziyi Pixels oluşturmak için özelliğini kullanmış olabilir:

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;
}

Dizinin dizininin pixels ve col değişkenlerinden row hesaplanması gerektiğini unutmayın. Satır, her satırdaki piksel sayısıyla (bu örnekte 256) çarpılır ve sütun eklenir.

SKBitmap tüm bit eşlem için bayt dizisi döndüren benzer Bytes bir özellik de tanımlar, ancak tam renkli bit eşlemler için daha hantaldır.

GetPixels işaretçisi

Bit eşlem piksellerine erişmek için en güçlü teknik, yöntemi veya Pixels özelliğiyle GetPixel karıştırılmamasıdırGetPixels. C# programlamasında çok yaygın olmayan bir şey döndürmesiyle ilgili bir farkı GetPixels hemen fark edeceksiniz:

IntPtr pixelsAddr = bitmap.GetPixels();

.NET IntPtr türü bir işaretçiyi temsil eder. Genellikle 32 bit veya 64 bit uzunluğunda olmak üzere programın çalıştırıldığı makinenin yerel işlemcisindeki bir tamsayı uzunluğu olduğundan çağrılır IntPtr . IntPtr DöndürenGetPixels, bit eşlem nesnesinin piksellerini depolamak için kullandığı gerçek bellek bloğunun adresidir.

yöntemini kullanarak öğesini IntPtr bir C# işaretçi türüne ToPointer dönüştürebilirsiniz. C# işaretçisi söz dizimi C ve C++ile aynıdır:

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

Değişken ptr bayt işaretçisi türündedir. Bu ptr değişken bit eşlem piksellerini depolamak için kullanılan tek tek bellek baytlarına erişmenizi sağlar. Bu bellekten bayt okumak veya belleğe bayt yazmak için aşağıdaki gibi bir kod kullanırsınız:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

Bu bağlamda yıldız işareti C# dolaylı işlecidir ve tarafından ptrişaret edilen belleğin içeriğine başvurmak için kullanılır. Başlangıçta, ptr bit eşlemin ilk satırının ilk pikselinin ilk baytını işaret eder, ancak bit eşlem içindeki diğer konumlara taşımak için değişken üzerinde ptr aritmetik gerçekleştirebilirsiniz.

Bir dezavantajı, bu ptr değişkeni yalnızca anahtar sözcüğüyle işaretlenmiş bir kod bloğunda unsafe kullanabilmenizdir. Ayrıca, derleme güvenli olmayan bloklara izin verecek şekilde işaretlenmelidir. Bu, projenin özelliklerinde yapılır.

C# dilinde işaretçileri kullanmak çok güçlüdür, ancak aynı zamanda çok tehlikelidir. İşaretçinin başvurması gereken bellek dışında belleğe erişmemeye dikkat etmeniz gerekir. bu nedenle işaretçi kullanımı "güvenli değil" sözcüğüyle ilişkilendirilir.

Sınıfındaki GradientBitmapPage yöntemini kullanan yöntem aşağıdadır GetPixels . Bayt işaretçisini unsafe kullanarak tüm kodu kapsayan bloğuna dikkat edin:

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;
}

ptr değişkeni yönteminden ToPointer ilk kez elde edildiğinde, bit eşlemin ilk satırının en soldaki pikselinin ilk baytını gösterir. ve döngülerirow, her pikselin ++ her baytı ayarlandıktan sonra işleçle artırılacak şekilde ayarlanırptr.col for Diğer 99 piksel döngüleri için bit ptr eşlem başlangıcına geri ayarlanmalıdır.

Her piksel dört bayt bellek olduğundan her bayt ayrı olarak ayarlanmalıdır. Buradaki kod, baytların renk türüyle tutarlı olan kırmızı, yeşil, mavi ve alfa sıralarında SKColorType.Rgba8888 olduğunu varsayar. Bunun iOS ve Android için varsayılan renk türü olduğunu ancak Evrensel Windows Platformu için olmadığını hatırlayabilirsiniz. Varsayılan olarak, UWP renk türüyle SKColorType.Bgra8888 bit eşlemler oluşturur. Bu nedenle, bu platformda bazı farklı sonuçlar görmeyi bekleyebilirsiniz!

döndürülen değeri ToPointer işaretçi yerine byte bir uint işaretçiye vermek mümkündür. Bu, bir pikselin tamamına tek bir deyimde erişilmesine olanak tanır. ++ İşleci bu işaretçiye uygulamak, sonraki pikseli işaret etmek için bunu dört bayt artırır:

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);
    ···
}

Piksel, kırmızı, yeşil, mavi ve alfa bileşenlerinden bir tamsayı pikseli oluşturan yöntemi kullanılarak MakePixel ayarlanır. Biçimin SKColorType.Rgba8888 aşağıdaki gibi bir piksel bayt sıralamasına sahip olduğunu unutmayın:

RR GG BB AA

Ancak bu baytlara karşılık gelen tamsayı:

AABBGGRR

Tamsayının en az önemli baytı, little-endian mimarisine uygun olarak ilk olarak depolanır. Bu MakePixel yöntem, renk türüne Bgra8888 sahip bit eşlemler için düzgün çalışmaz.

MakePixel yöntemine, derleyiciyi MethodImplOptions.AggressiveInlining bunu ayrı bir yöntem yapmaktan kaçınmaya teşvik etme, bunun yerine yönteminin çağrıldığı kodu derleme seçeneğiyle bayrak eklenir. Bu, performansı geliştirmelidir.

İlginç bir şekilde, SKColor yapı öğesinden SKColor işaretsiz bir tamsayıya açık bir dönüştürme tanımlar; bu da bir SKColor değerin oluşturulabileceği ve yerine kullanılacak MakePixelbir dönüştürmenin uint kullanılabildiği anlamına gelir:

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;
}

Tek soru şudur: Değerin SKColor tamsayı biçimi renk türü veya renk türü sırasına SKColorType.Rgba8888 SKColorType.Bgra8888 göre mi yoksa tamamen başka bir şey mi? Bu sorunun cevabı kısa süre sonra ortaya çıkar.

SetPixels Yöntemi

SKBitmap ayrıca, aşağıdaki gibi çağırdığınız adlı SetPixelsbir yöntemi tanımlar:

bitmap.SetPixels(intPtr);

GetPixels Bit eşlem tarafından piksellerini depolamak için kullanılan bellek bloğuna başvuran bir IntPtr alan olduğunu hatırlayın. Çağrısı, SetPixels bu bellek bloğunu, belirtilen tarafından bağımsız değişken olarak başvuruda bulunan IntPtr bellek bloğuyla SetPixels değiştirir. Bit eşlem daha önce kullandığı bellek bloğunu boşaltıyor. Bir sonraki çağrıda GetPixels ile SetPixelsayarlanan bellek bloğunu alır.

İlk başta, daha az kullanışlı olduğunuzdan GetPixels daha fazla güç ve performans vermiyor gibi SetPixels görünüyor. Bit GetPixels eşlem bellek bloğunu elde ederek buna erişin. Biraz SetPixels bellek ayırıp erişerek bunu bit eşlem bellek bloğu olarak ayarlayın.

Ancak kullanmak SetPixels benzersiz bir bozulma avantajı sunar: Bit eşlem piksel bitlerine bir dizi kullanarak erişmenizi sağlar. Bu tekniği gösteren yöntemi GradientBitmapPage aşağıda bulabilirsiniz. yöntemi ilk olarak bit eşlem piksellerinin baytlarına karşılık gelen çok boyutlu bir bayt dizisi tanımlar. İlk boyut satırdır, ikinci boyut sütundur ve üçüncü boyut her pikselin dört bileşeniyle ilişkilendirilir:

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;
}

Ardından, dizi piksellerle doldurulduktan sonra, bu diziyi işaret eden bir unsafe fixed bayt işaretçisi elde etmek için bir blok ve bir deyim kullanılır. Bu bayt işaretçisi daha sonra öğesine geçirmek SetPixelsiçin öğesine IntPtr yayınlanabilir.

Oluşturduğunuz dizinin bayt dizisi olması gerekmez. Satır ve sütun için yalnızca iki boyutu olan bir tamsayı dizisi olabilir:

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;
}

MakePixel yöntemi, renk bileşenlerini 32 bit pikselde birleştirmek için yeniden kullanılır.

Tamlık için aynı kod şu şekildedir; ancak işaretsiz bir SKColor tamsayıya değer atanmalıdır:

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;
}

Teknikleri karşılaştırma

Gradyan Rengi sayfasının oluşturucusunun yukarıda gösterilen sekiz yöntemin tümünü çağırması ve sonuçları kaydetmesi:

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;
    }
    ···
}

Oluşturucu, sonuçta elde edilen bit eşlemleri görüntülemek için bir SKCanvasView oluşturarak sonuçlanır. İşleyici, PaintSurface yüzeyini sekiz dikdörtgene böler ve her birini görüntülemek için çağırır Display :

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);
        }
    }
}

Derleyicinin kodu iyileştirmesine izin vermek için bu sayfa Yayın modunda çalıştırıldı. MacBook Pro, Nexus 5 Android telefon ve Windows 10 çalıştıran Surface Pro 3'te i Telefon 8 simülatöründe çalışan sayfa aşağıdadır. Donanım farklılıkları nedeniyle, cihazlar arasındaki performans sürelerini karşılaştırmaktan kaçının, ancak bunun yerine her cihazdaki göreli sürelere bakın:

Gradyan Bit Eşlem

Yürütme sürelerini milisaniye cinsinden bir araya getiren bir tablo aşağıdadır:

API Veri türü iOS Android UWP
SetPixel 3.17 10.77 3.49
Piksel 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

Beklendiği gibi 65.536 kez çağrılması SetPixel , bit eşlem piksellerini ayarlamanın en düşük verimli yoludur. Bir SKColor diziyi Pixels doldurmak ve özelliği ayarlamak çok daha iyidir ve hatta bazı GetPixels ve SetPixels teknikleriyle karşılaştırır. Piksel değerleriyle uint çalışmak genellikle ayrı byte bileşenler ayarlamaktan daha hızlıdır ve değeri işaretsiz bir tamsayıya dönüştürmek SKColor işleme biraz ek yük getirir.

Çeşitli gradyanları karşılaştırmak da ilginçtir: Her platformun en üst satırları aynıdır ve gradyan amaçlandığı gibi gösterilir. Bu, yöntemin SetPixel ve özelliğin Pixels temel piksel biçiminden bağımsız olarak renklerden pikselleri doğru şekilde oluşturduğu anlamına gelir.

iOS ve Android ekran görüntülerinin sonraki iki satırı da aynıdır ve bu da küçük MakePixel yöntemin bu platformlar için varsayılan Rgba8888 piksel biçimi için doğru tanımlandığını onaylar.

iOS ve Android ekran görüntülerinin alt satırı geriye doğrudur ve bu, bir SKColor değer atanarak elde edilen imzasız tamsayının formda olduğunu gösterir:

AARRGGBB

Baytlar şu sıradadır:

BB GG RR AA

Bu, Bgra8888 sıralama yerine sıralamadır Rgba8888 . Biçim Brga8888 , Evrensel Windows platformu için varsayılan değerdir; bu nedenle bu ekran görüntüsünün son satırındaki gradyanlar ilk satırla aynıdır. Ancak bu bit eşlemleri oluşturan kod bir Rgba8888 sıralama varsaydığından ortadaki iki satır yanlıştır.

Her platformda piksel bitlerine erişmek için aynı kodu kullanmak istiyorsanız, veya Bgra8888 biçimini kullanarak Rgba8888 açıkça bir SKBitmap oluşturabilirsiniz. Değerleri bit eşlem piksellerine değiştirmek SKColor istiyorsanız kullanın Bgra8888.

Piksellerin rastgele erişimi

FillBitmapBytePtr Gradyan Bit Eşlem sayfasındaki ve FillBitmapUintPtr yöntemleri, bit eşlemi sıralı olarak, üst satırdan alt satıra ve her satırda soldan sağa doldurmak için tasarlanmış döngülerden yararlandıfor. Piksel, işaretçiyi artıran aynı deyimle ayarlanabilir.

Bazen piksellere sırayla değil rastgele erişmek gerekir. Yaklaşımı kullanıyorsanız GetPixels , işaretçileri satır ve sütuna göre hesaplamanız gerekir. Bu, gökkuşağını sinüs eğrisinin bir döngüsü biçiminde gösteren bir bit eşlem oluşturan Gökkuşağı Sinüsü sayfasında gösterilmiştir.

Gökkuşağının renkleri, HSL (ton, doygunluk, parlaklık) renk modeli kullanılarak en kolay şekilde oluşturulur. yöntemi, SKColor.FromHsl 0 ile 360 arasında (bir dairenin açıları gibi, kırmızıdan yeşile ve maviye ve yeniden kırmızıya) ve 0 ile 100 arasında doygunluk ve parlaklık değerleri kullanarak bir SKColor değer oluşturur. Gökkuşağının renkleri için doygunluk en fazla 100'e, parlaklık ise 50 orta noktasına ayarlanmalıdır.

Gökkuşağı Sinüsü , bit eşlem satırlarında döngü yaparak ve ardından 360 ton değeri arasında döngü yaparak bu görüntüyü oluşturur. Her ton değerinden, sinüs değerini de temel alan bir bit eşlem sütununu hesaplar:

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);
    }
}

Oluşturucunun bit eşlemi biçimine göre oluşturduğuna SKColorType.Bgra8888 dikkat edin:

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

Bu, programın endişelenmeden değerleri piksellere uint dönüştürmesini SKColor sağlar. Bu programda bir rol oynamasa da, pikselleri ayarlamak için dönüştürmeyi SKColor her kullandığınızda, renk bileşenlerini alfa değerine göre önceden yüklemediğinden de belirtmeniz SKColor SKAlphaType.Unpremul gerekir.

Oluşturucu daha sonra yöntemini kullanarak GetPixels bit eşlemin ilk pikseline bir işaretçi alır:

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

Belirli bir satır ve sütun için, öğesine basePtrbir uzaklık değeri eklenmelidir. Bu uzaklık, satırın bit eşlem genişliğinin yanı sıra sütunun çarpı olmasıdır:

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

Bu SKColor işaretçi kullanılarak değer bellekte depolanır:

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

PaintSurface işleyicisinde SKCanvasViewbit eşlem, görüntü alanını dolduracak şekilde genişletilir:

Gökkuşağı Sinüsü

Bir bit eşlemden diğerine

Çok sayıda görüntü işleme görevi, pikselleri bir bit eşlemden diğerine aktarılırken değiştirmeyi içerir. Bu teknik, Renk Ayarlaması sayfasında gösterilmiştir. Sayfa bit eşlem kaynaklarından birini yükler ve ardından üç Slider görünüm kullanarak görüntüyü değiştirmenize olanak tanır:

Renk Ayarlama

Her piksel rengi için ilk Slider değer tona 0 ile 360 arasında bir değer ekler, ancak ardından sonucu 0 ile 360 arasında tutmak için modül işlecini kullanarak renkleri spektrum boyunca etkili bir şekilde kaydırır (UWP ekran görüntüsünde gösterildiği gibi). İkincisi Slider , doygunluğa uygulamak için 0,5 ile 2 arasında bir çarpımsal faktör seçmenize olanak tanır ve üçüncüsü Slider , Android ekran görüntüsünde gösterildiği gibi parlaklık için aynı işlemi yapar.

Program, adlı özgün kaynak bit eşlem ve adlı srcBitmap ayarlanmış hedef bit eşlem olan dstBitmapiki bit eşlem tutar. bir Slider her taşındığında, program içindeki dstBitmaptüm yeni pikselleri hesaplar. Elbette, kullanıcılar görünümleri çok hızlı bir şekilde taşıyarak Slider denemeler yapacaktır, böylece yönetebileceğiniz en iyi performansı istiyorsunuz. Bu, hem kaynak hem de hedef bit eşlemler için yöntemini içerir GetPixels .

Renk Ayarlama sayfası, kaynak ve hedef bit eşlemlerin renk biçimini denetlemez. Bunun yerine ve biçimleri için SKColorType.Rgba8888 SKColorType.Bgra8888 biraz farklı mantık içerir. Kaynak ve hedef farklı biçimlerde olabilir ve program çalışmaya devam eder.

Kaynağın piksellerini hedefe aktaran kritik TransferPixels yöntem dışındaki program aşağıdadır. Oluşturucu, değerine srcBitmapeşit ayarlardstBitmap. İşleyici PaintSurface görüntüler 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);
    }
}

Görünümlerin ValueChanged Slider işleyicisi ayarlama değerlerini hesaplar ve çağırır TransferPixels.

Yöntemin tamamı TransferPixels olarak unsafeişaretlenir. Her iki bit eşlemlerin piksel bitlerine bayt işaretçileri alarak başlar ve ardından tüm satırlar ve sütunlar arasında döngüler oluşturur. Yöntem, kaynak bit eşleminden her piksel için dört bayt alır. Bunlar veya Bgra8888 sırasına göre Rgba8888 olabilir. Renk türünü denetlemek, bir SKColor değerin oluşturulmasına olanak tanır. HSL bileşenleri daha sonra ayıklanır, ayarlanır ve değeri yeniden oluşturmak SKColor için kullanılır. Hedef bit eşleminin Rgba8888 veya Bgra8888olmasına bağlı olarak baytlar hedef bit damgasında depolanır:

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;
                }
            }
        }
    }
    ···
}

Bu yöntemin performansı, kaynak ve hedef bit eşlemlerin renk türlerinin çeşitli bileşimleri için ayrı yöntemler oluşturularak ve her piksel için türü denetlemekten kaçınılarak daha da iyileştirilebilir. Bir diğer seçenek de, renk türüne göre değişken için col birden çok for döngüye sahip olmaktır.

Posterleştirme

Piksel bitlerine erişmeyi içeren bir diğer yaygın iş de posterleştirmedir. Bit eşlem piksellerinde kodlanmış renkler azaltılır ve sonuç sınırlı renk paleti kullanılarak elle çizilmiş postere benzer.

Posterize sayfası bu işlemi maymun görüntülerinden birinde gerçekleştirir:

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;
    }
}

Oluşturucudaki kod her piksele erişir, 0xE0E0E0FF değeriyle bit düzeyinde AND işlemi gerçekleştirir ve ardından sonucu bit eşlem içinde yeniden depolar. 0xE0E0E0FF değerler her renk bileşeninin yüksek 3 bitini tutar ve alt 5 biti 0 olarak ayarlar. Bit eşlem 2 24 veya 16.777.216 renk yerine 29 veya 512 renge indirilir:

İki mobil cihazda ve masaüstü penceresinde bir oyuncak maymunun posterize görüntüsünü gösteren ekran görüntüsü.