Доступ к битам растрового изображения SkiaSharp

Как вы видели в статье Сохранение растровых карт SkiaSharp в файлы, растровые изображения обычно хранятся в файлах в сжатом формате, например JPEG или PNG. В констрастном виде растровое изображение SkiaSharp, хранящееся в памяти, не сжимается. Он хранится в виде последовательного ряда пикселей. Этот несжатый формат упрощает передачу растровых изображений в область отображения.

Блок памяти, занятый растровым изображением SkiaSharp, организован очень просто: начинается с первой строки пикселей, от левой до правой, а затем продолжается со второй строки. Для полноцветных растровых изображений каждый пиксель состоит из четырех байтов, что означает, что общее пространство памяти, требуемое растровым изображением, составляет четыре раза больше ширины и высоты.

В этой статье описывается, как приложение может получить доступ к этим пикселям, напрямую путем доступа к блоку памяти пикселя растрового изображения или косвенно. В некоторых случаях программа может потребоваться проанализировать пиксели изображения и создать гистограмму определенного рода. Чаще всего приложения могут создавать уникальные изображения, алгоритмически создавая пиксели, составляющие растровое изображение:

Примеры битов пикселей

Методы

SkiaSharp предоставляет несколько методов доступа к битам пикселей растрового изображения. Какой из вариантов вы выбираете, обычно является компромиссом между удобством написания кода (что связано с обслуживанием и простотой отладки) и производительностью. В большинстве случаев вы будете использовать один из следующих методов и свойств SKBitmap для доступа к пикселям растрового изображения:

  • SetPixel Методы GetPixel позволяют получить или задать цвет одного пикселя.
  • Свойство Pixels получает массив цветов пикселей для всего растрового изображения или задает массив цветов.
  • GetPixels возвращает адрес памяти пикселя, используемой растровым изображением.
  • SetPixels заменяет адрес памяти пикселя, используемой растровым изображением.

Вы можете думать о первых двух методах как "высокий уровень" и второй как "низкий уровень". Существуют некоторые другие методы и свойства, которые можно использовать, но это наиболее ценные.

Чтобы увидеть различия в производительности этих методов, пример приложения содержит страницу с именем Gradient Bitmap, которая создает растровое изображение с пикселями, которые объединяют красные и голубые оттенки для создания градиента. Программа создает восемь разных копий этой растровой карты, все с помощью различных методов настройки растровых пикселей. Каждая из этих восьми растровых карт создается в отдельном методе, который также задает краткое описание метода и вычисляет время, необходимое для задания всех пикселей. Каждый метод проходит по логике настройки пикселей 100 раз, чтобы получить лучшую оценку производительности.

Метод SetPixel

Если вам нужно задать или получить несколько отдельных пикселей, SetPixelGetPixel методы идеально подходят. Для каждого из этих двух методов указывается целый столбец и строка. Независимо от формата пикселей, эти два метода позволяют получить или задать пиксель в качестве SKColor значения:

bitmap.SetPixel(col, row, color);

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

Аргумент col должен иметь диапазон от 0 до одного меньше Width свойства растрового рисунка и row диапазон от 0 до одного меньше Height свойства.

Ниже приведен метод в Градиентном битовом SetPixel рисунке, который задает содержимое растрового изображения с помощью метода. Растровое изображение составляет 256 к 256 пикселям, а for циклы жестко закодируются диапазоном значений:

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

Набор цветов для каждого пикселя имеет красный компонент, равный столбцу растрового изображения, и синий компонент, равный строке. Результатом растрового изображения является черный в левом верхнем углу, красный в правом верхнем углу, синий в левом нижнем углу и градиенты в нижнем правом углу, с градиентами в другом месте.

Метод SetPixel вызывается 65,536 раз, и независимо от того, насколько эффективным этот метод может быть, обычно не рекомендуется делать так много вызовов API, если альтернатива доступна. К счастью, есть несколько альтернатив.

Свойство "Пиксели"

SKBitmap определяет Pixels свойство, которое возвращает массив значений для всего растрового SKColor изображения. Можно также использовать Pixels для задания массива значений цветов для растрового изображения:

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Пиксели расположены в массиве, начиная с первой строки, слева направо, а затем второй строки и т. д. Общее количество цветов в массиве равно продукту ширины и высоты растрового изображения.

Хотя это свойство, как представляется, эффективно, помните, что пиксели копируются из растрового изображения в массив, а из массива обратно в растровое изображение, а пиксели преобразуются из и в SKColor значения.

Вот метод в GradientBitmapPage классе, который задает растровое изображение с помощью Pixels свойства. Метод выделяет SKColor массив требуемого размера, но он мог бы использовать Pixels свойство для создания этого массива:

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

Обратите внимание, что индекс массива pixels необходимо вычислить из row переменных и col переменных. Строка умножается на количество пикселей в каждой строке (256 в данном случае), а затем добавляется столбец.

SKBitmap также определяет аналогичное Bytes свойство, которое возвращает массив байтов для всего растрового изображения, но это более сложно для полноцветных растровых изображений.

Указатель GetPixels

Потенциально наиболее мощный способ доступа к растровым пикселям заключается в GetPixelsтом, чтобы не путать с GetPixel методом или свойством Pixels . Вы сразу заметите разницу в GetPixels том, что она возвращает что-то не очень распространенное в программировании C#:

IntPtr pixelsAddr = bitmap.GetPixels();

Тип .NET IntPtr представляет указатель. Он вызывается IntPtr потому, что это длина целого числа на собственном процессоре компьютера, на котором выполняется программа, как правило, 32 бита или 64 бита. GetPixels Это IntPtr адрес фактического блока памяти, используемого объектом растрового изображения для хранения пикселей.

Тип указателя C# можно преобразовать IntPtr с помощью ToPointer метода. Синтаксис указателя C# совпадает с синтаксисом C и C++:

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

Переменная ptr имеет указатель типа байтов. Эта ptr переменная позволяет получить доступ к отдельным байтам памяти, используемым для хранения пикселей растрового изображения. Этот код используется для чтения байта из этой памяти или записи байта в память:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

В этом контексте звездочка является оператором косвенного обращения C#и используется для ссылки на содержимое памяти, на которую указываетptr. ptr Изначально указывает на первый байт первого пикселя первой строки растрового изображения, но вы можете выполнить арифметику ptr переменной, чтобы переместить ее в другие расположения в растровом рисунке.

Одним из недостатков является то, что эту ptr переменную можно использовать только в блоке кода, помеченном unsafe ключевое слово. Кроме того, сборка должна быть помечена как разрешающая небезопасные блоки. Это делается в свойствах проекта.

Использование указателей в C# очень мощный, но и очень опасный. Необходимо быть осторожным, что вы не обращаетесь к памяти за пределами того, что указатель должен ссылаться. Именно поэтому использование указателя связано с словом "небезопасным".

Вот метод в GradientBitmapPage классе, который использует GetPixels метод. Обратите внимание на unsafe блок, охватывающий весь код с помощью указателя байтов:

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 При первом получении переменной из ToPointer метода он указывает на первый байт левого пикселя первой строки растрового изображения. Циклы for для row и col настроены таким образом, чтобы ptr можно было увеличить ++ оператор после каждого байта каждого пикселя. Для других 99 циклов через пиксели ptr необходимо вернуться к началу растрового изображения.

Каждый пиксель составляет четыре байта памяти, поэтому каждый байт должен быть задан отдельно. В коде предполагается, что байты находятся в порядке красного, зеленого, синего и альфа-цвета, который соответствует типу SKColorType.Rgba8888 цвета. Вы можете вспомнить, что это тип цвета по умолчанию для iOS и Android, но не для универсальная платформа Windows. По умолчанию UWP создает растровые изображения с типом SKColorType.Bgra8888 цвета. По этой причине ожидается увидеть некоторые различные результаты на этой платформе!

Можно привести значение, возвращаемое от ToPointeruint указателя, а не byte указателя. Это позволяет получить доступ ко всему пикселю в одной инструкции. ++ Применение оператора к указателю увеличивает его на четыре байта, чтобы указать на следующий пиксель:

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

Пиксель задается с помощью MakePixel метода, который создает целочисленный пиксель из красных, зеленых, синих и альфа-компонентов. Помните, что формат SKColorType.Rgba8888 имеет порядок байтов пикселей следующим образом:

RR GG BB AA

Но целое число, соответствующее этим байтам:

AABBGGRR

Наименьший байт целочисленного числа хранится в первую очередь в соответствии с малой архитектурой. Этот MakePixel метод не будет работать правильно для растровых изображений с типом Bgra8888 цвета.

Этот MakePixel метод помечается параметром MethodImplOptions.AggressiveInlining , чтобы поощрять компилятора, чтобы избежать создания этого отдельного метода, а вместо этого компилировать код, в котором вызывается метод. Это должно повысить производительность.

Интересно, SKColor что структура определяет явное преобразование из SKColor целого числа без знака, что означает, что SKColor можно создать значение, а преобразование, которое uint можно использовать вместо 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;
}

Единственным вопросом является следующее: является ли целочисленный формат SKColor значения в порядке SKColorType.Rgba8888 типа цвета, или типа цвета, или SKColorType.Bgra8888 это что-то другое? Ответ на этот вопрос должен быть показан в ближайшее время.

Метод SetPixels

SKBitmap также определяет метод с именем SetPixels, который вызывается следующим образом:

bitmap.SetPixels(intPtr);

Помните, что GetPixels получает ссылку на IntPtr блок памяти, используемый растровым изображением для хранения пикселей. Вызов SetPixels заменяет этот блок памяти блоком памяти, на который ссылается указанный IntPtr в качестве аргумента SetPixels. Затем растровое изображение освобождает блок памяти, который он использовал ранее. При следующем GetPixels вызове он получает набор блоков памяти с SetPixels.

Во-первых, кажется, что вы SetPixels не даете больше энергии и производительности, чем GetPixels в то время как быть менее удобным. При GetPixels получении блока памяти растрового изображения и доступа к нему. При SetPixels выделении и доступе к некоторой памяти, а затем задайте его в качестве блока памяти растрового изображения.

Но использование SetPixels предлагает уникальное синтаксическое преимущество: он позволяет получить доступ к битам пикселей растрового изображения с помощью массива. Ниже приведен метод, GradientBitmapPage демонстрирующий этот метод. Метод сначала определяет многомерный массив байтов, соответствующий байтам пикселей растрового изображения. Первое измерение — строка, второе измерение — столбец, а третье измерение корресондирует до четырех компонентов каждого пикселя:

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

Затем после заполнения массива пикселями unsafe блок и fixed оператор используется для получения указателя байтов, который указывает на этот массив. Затем этот указатель байтов можно привести к IntPtr передаче SetPixels.

Создаваемый массив не должен быть массивом байтов. Это может быть целый массив с двумя измерениями для строки и столбца:

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 снова используется для объединения компонентов цвета в 32-разрядный пиксель.

Только для полноты вот тот же код, но со SKColor значением, приведенным в целое число без знака:

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

Сравнение методов

Конструктор страницы Градиента цвета вызывает все восемь методов, показанных выше, и сохраняет результаты:

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

Конструктор завершает создание SKCanvasView результирующих растровых изображений. Обработчик PaintSurface делит ее поверхность на восемь прямоугольников и вызовы 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);
        }
    }
}

Чтобы разрешить компилятору оптимизировать код, эта страница была запущена в режиме выпуска . Вот эта страница работает на симуляторе i Телефон 8 на MacBook Pro, телефоне с Android Nexus 5 и Surface Pro 3 под управлением Windows 10. Из-за различий оборудования избегайте сравнения времени производительности между устройствами, но вместо этого посмотрите на относительное время на каждом устройстве:

Градиентная растровая карта

Ниже приведена таблица, которая объединяет время выполнения в миллисекундах:

API Тип данных iOS Android UWP
SetPixel 3,17 10,77 3.49
Пиксели 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

Как ожидалось, вызов SetPixel 65 536 раз является наименее эффективным способом задания пикселей растрового изображения. Заполнение массива SKColor и настройка Pixels свойства гораздо лучше, и даже сравнивается с некоторыми из GetPixels методов и SetPixels методов. Работа со uint значениями пикселей обычно выполняется быстрее, чем установка отдельных byte компонентов, а преобразование SKColor значения в целое число без знака добавляет некоторые расходы на процесс.

Кроме того, интересно сравнить различные градиенты: верхние строки каждой платформы одинаковы, и показать градиент, как он был предназначен. Это означает, что SetPixel метод и Pixels свойство правильно создают пиксели из цветов независимо от базового формата пикселей.

Следующие две строки снимок экрана iOS и Android также одинаковы, что подтверждает правильность правильного определения небольшого MakePixel метода для формата пикселей по умолчанию Rgba8888 для этих платформ.

В нижней строке снимка экрана iOS и Android отображается обратная строка, указывающая, что целое число без знака, полученное путем приведения SKColor значения в форму:

AARRGGBB

Байты находятся в порядке:

BB GG RR AA

Это упорядочение Bgra8888 , а не Rgba8888 порядок. Формат Brga8888 по умолчанию для универсальной платформы Windows, поэтому градиенты на последней строке этого снимка экрана совпадают с первой строкой. Но средние две строки неверны, так как код, создающий эти растровые изображения, предполагает упорядочение Rgba8888 .

Если вы хотите использовать один и тот же код для доступа к битам пикселей на каждой платформе, вы можете явно создать его SKBitmap с помощью Rgba8888 или Bgra8888 формата. Если вы хотите привести SKColor значения к растровым пикселям, используйте Bgra8888.

Случайный доступ к пикселям

Методы FillBitmapBytePtr и методы на странице Градиентной растровой карты извлекаются из for циклов, предназначенных для последовательных заполнения растрового изображения, от верхней до нижней строки и каждой строки слева направо.FillBitmapUintPtr Пиксель можно задать с той же инструкцией, которая увеличивает указатель.

Иногда необходимо получить доступ к пикселям случайным образом, а не последовательно. Если вы используете GetPixels подход, необходимо вычислить указатели на основе строки и столбца. Это демонстрируется на странице Радуга Синус , которая создает растровое изображение, показывающее радугу в виде одного цикла синусовой кривой.

Цвета радуги проще всего создавать с помощью цветовой модели HSL (тон, насыщенность, светимость). Метод SKColor.FromHsl создает SKColor значение с помощью значений оттенка, которые варьируются от 0 до 360 (например, углов круга, но переходят от красного, зеленого и синего и обратно к красному), а также значения насыщенности и светимости от 0 до 100. Для цветов радуги насыщенность должна быть задана не более 100, а светимость — в середине 50.

Радуга Sine создает это изображение, циклизуясь по строкам растрового изображения, а затем циклично 360 значений оттенков. По каждому значению hue вычисляется столбец растрового изображения, который также основан на значении синуса:

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

Обратите внимание, что конструктор создает растровое изображение на SKColorType.Bgra8888 основе формата:

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

Это позволяет программе использовать преобразование значений в uint пиксели SKColor без беспокойства. Хотя она не играет роль в этой конкретной программе, всякий раз, когда вы используете SKColor преобразование для задания пикселей, следует также указать SKAlphaType.Unpremul , так как SKColor не предварительно определяет его цветные компоненты по альфа-значению.

Затем конструктор использует GetPixels метод для получения указателя на первый пиксель растрового изображения:

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

Для любой конкретной строки и столбца необходимо добавить basePtrзначение смещения. Это смещение — это время строки ширины растрового изображения, а также столбец:

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

Значение SKColor хранится в памяти с помощью этого указателя:

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

В обработчике PaintSurfaceSKCanvasViewрастрового изображения растягивается для заполнения области отображения:

Радуга Синус

Из одной растровой карты в другую

Очень многие задачи обработки изображений включают изменение пикселей по мере их передачи из одной растровой карты в другую. Этот метод показан на странице "Корректировка цвета". Страница загружает один из ресурсов растрового изображения, а затем позволяет изменять изображение с помощью трех Slider представлений:

Корректировка цвета

Для каждого цвета пикселя первый Slider добавляет значение от 0 до 360 к оттенку, но затем использует оператор модуля, чтобы сохранить результат в диапазоне от 0 до 360, эффективно сдвигая цвета вдоль спектра (как демонстрируется снимок экрана UWP). Slider Второй позволяет выбрать мультипликативный фактор от 0,5 до 2, чтобы применить к насыщенности, а третий Slider делает то же самое для светимости, как показано на снимке экрана Android.

Программа поддерживает две растровые карты, исходное исходное растровое изображение с именем srcBitmap и измененное целевое растровое dstBitmapизображение. Slider При каждом перемещении программа вычисляет все новые пиксели в dstBitmap. Конечно, пользователи будут экспериментировать, перемещая Slider представления очень быстро, поэтому вы хотите, чтобы лучшая производительность вы могли управлять. Это включает GetPixels метод для исходных и целевых растровых изображений.

Страница "Корректировка цвета" не управляет цветовым форматом исходных и целевых растровых изображений. Вместо этого она содержит немного другую логику для SKColorType.Rgba8888 и SKColorType.Bgra8888 форматов. Исходный и целевой форматы могут быть разными, и программа по-прежнему будет работать.

Вот программа, за исключением решающего TransferPixels метода, который передает пиксели формы источника в место назначения. Конструктор задает dstBitmap равный srcBitmap. Обработчик PaintSurface отображает 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);
    }
}

Обработчик ValueChanged представлений Slider вычисляет значения и вызовы TransferPixelsкорректировки.

Весь TransferPixels метод помечается как unsafe. Он начинается с получения указателей байтов на биты пикселей обоих растровых изображений, а затем циклически проходит по всем строкам и столбцам. Из исходной растровой карты метод получает четыре байта для каждого пикселя. Они могут находиться в любом Rgba8888Bgra8888 порядке. Проверка типа цвета позволяет SKColor создать значение. Затем компоненты HSL извлекаются, корректируются и используются для повторного SKColor создания значения. В зависимости от того, является Rgba8888 ли целевой растровый рисунок или Bgra8888, байты хранятся в целевой битовойmp:

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

Скорее всего, производительность этого метода может быть улучшена еще больше, создавая отдельные методы для различных сочетаний цветов исходных и целевых растровых карт и избегайте проверка типа для каждого пикселя. Другой вариант — иметь несколько for циклов для переменной col на основе типа цвета.

Постеризации

Другим общим заданием, которое включает доступ к битам пикселей, является плакатизация. Число, если цвета, закодированные в пикселях растрового изображения, сокращаются, чтобы результат напоминал рукописный плакат с помощью ограниченной цветовой палитры.

Страница "Плакатизация " выполняет этот процесс на одном из изображений обезьяны:

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

Код в конструкторе обращается к каждому пикселю, выполняет побитовую операцию AND со значением 0xE0E0E0FF, а затем сохраняет результат обратно в растровом рисунке. Значения 0xE0E0E0FF сохраняют высокий уровень 3 бита каждого компонента цвета и задают меньше 5 битов 0. Вместо 224 или 16 777 216 цветов растровое изображение уменьшается до 29 или 512 цветов:

Снимок экрана: плакать изображение обезьяны на двух мобильных устройствах и в окне рабочего стола.