Доступ к битам растрового изображения SkiaSharp
Как вы видели в статье Сохранение растровых карт SkiaSharp в файлы, растровые изображения обычно хранятся в файлах в сжатом формате, например JPEG или PNG. В констрастном виде растровое изображение SkiaSharp, хранящееся в памяти, не сжимается. Он хранится в виде последовательного ряда пикселей. Этот несжатый формат упрощает передачу растровых изображений в область отображения.
Блок памяти, занятый растровым изображением SkiaSharp, организован очень просто: начинается с первой строки пикселей, от левой до правой, а затем продолжается со второй строки. Для полноцветных растровых изображений каждый пиксель состоит из четырех байтов, что означает, что общее пространство памяти, требуемое растровым изображением, составляет четыре раза больше ширины и высоты.
В этой статье описывается, как приложение может получить доступ к этим пикселям, напрямую путем доступа к блоку памяти пикселя растрового изображения или косвенно. В некоторых случаях программа может потребоваться проанализировать пиксели изображения и создать гистограмму определенного рода. Чаще всего приложения могут создавать уникальные изображения, алгоритмически создавая пиксели, составляющие растровое изображение:
Методы
SkiaSharp предоставляет несколько методов доступа к битам пикселей растрового изображения. Какой из вариантов вы выбираете, обычно является компромиссом между удобством написания кода (что связано с обслуживанием и простотой отладки) и производительностью. В большинстве случаев вы будете использовать один из следующих методов и свойств SKBitmap
для доступа к пикселям растрового изображения:
SetPixel
МетодыGetPixel
позволяют получить или задать цвет одного пикселя.- Свойство
Pixels
получает массив цветов пикселей для всего растрового изображения или задает массив цветов. GetPixels
возвращает адрес памяти пикселя, используемой растровым изображением.SetPixels
заменяет адрес памяти пикселя, используемой растровым изображением.
Вы можете думать о первых двух методах как "высокий уровень" и второй как "низкий уровень". Существуют некоторые другие методы и свойства, которые можно использовать, но это наиболее ценные.
Чтобы увидеть различия в производительности этих методов, пример приложения содержит страницу с именем Gradient Bitmap, которая создает растровое изображение с пикселями, которые объединяют красные и голубые оттенки для создания градиента. Программа создает восемь разных копий этой растровой карты, все с помощью различных методов настройки растровых пикселей. Каждая из этих восьми растровых карт создается в отдельном методе, который также задает краткое описание метода и вычисляет время, необходимое для задания всех пикселей. Каждый метод проходит по логике настройки пикселей 100 раз, чтобы получить лучшую оценку производительности.
Метод SetPixel
Если вам нужно задать или получить несколько отдельных пикселей, SetPixel
GetPixel
методы идеально подходят. Для каждого из этих двух методов указывается целый столбец и строка. Независимо от формата пикселей, эти два метода позволяют получить или задать пиксель в качестве 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
цвета. По этой причине ожидается увидеть некоторые различные результаты на этой платформе!
Можно привести значение, возвращаемое от ToPointer
uint
указателя, а не 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);
В обработчике PaintSurface
SKCanvasView
растрового изображения растягивается для заполнения области отображения:
Из одной растровой карты в другую
Очень многие задачи обработки изображений включают изменение пикселей по мере их передачи из одной растровой карты в другую. Этот метод показан на странице "Корректировка цвета". Страница загружает один из ресурсов растрового изображения, а затем позволяет изменять изображение с помощью трех 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
. Он начинается с получения указателей байтов на биты пикселей обоих растровых изображений, а затем циклически проходит по всем строкам и столбцам. Из исходной растровой карты метод получает четыре байта для каждого пикселя. Они могут находиться в любом Rgba8888
Bgra8888
порядке. Проверка типа цвета позволяет 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 цветов: