存取 SkiaSharp 位圖圖圖元位
如將 SkiaSharp 位圖儲存至檔案一文中所見,位圖通常會以壓縮格式儲存在檔案中,例如 JPEG 或 PNG。 在 constrast 中,不會壓縮儲存在記憶體中的 SkiaSharp 位陣陣圖。 它會儲存為循序數列的圖元。 這個未壓縮的格式有助於將點陣圖傳輸至顯示介面。
SkiaSharp 位陣圖所佔用的記憶體區塊會以非常直接的方式組織:它會從左到右的第一個像素數據列開始,然後繼續進行第二個數據列。 對於全色位圖,每個圖元都包含四個字節,這表示位圖所需的總記憶體空間是其寬度和高度的四倍。
本文說明應用程式如何透過存取位圖的圖元記憶體區塊,或間接存取這些圖元。 在某些情況下,程式可能會想要分析影像的圖元,並建構某種類型的直方圖。 更常見的是,應用程式可以藉由以演算法方式建立構成位圖的圖元來建構唯一影像:
技術
SkiaSharp 提供數種技術來存取位圖的圖元。 您選擇的通常是程式代碼撰寫便利性(與維護和偵錯容易相關)和效能之間的妥協。 在大部分情況下,您將使用 下列其中一種方法和屬性 SKBitmap
來存取位圖的圖元:
GetPixel
和SetPixel
方法可讓您取得或設定單一像素的色彩。- 屬性
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
。
以下是 Gradient Bitmap 中使用 SetPixel
方法設定位圖內容的方法。 點陣圖為 256 x 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 次,無論此方法的效率如何,如果替代方法可用,通常不是一個好主意。 幸運的是,有幾個替代方案。
Pixels 屬性
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位長度。 IntPtr
傳GetPixels
回的 是位圖對象用來儲存其像素的實際記憶體區塊位址。
您可以使用 方法,將轉換成 IntPtr
C# 指標類型 ToPointer
。 C# 指標語法與 C 和 C++ 相同:
byte* ptr = (byte*)pixelsAddr.ToPointer();
變數 ptr
的類型為 位元組指標。 此 ptr
變數可讓您存取用來儲存點陣圖圖元的個別記憶體位元組。 您可以使用這類程式代碼從此記憶體讀取位元組,或將位元組寫入記憶體:
byte pixelComponent = *ptr;
*ptr = pixelComponent;
在此內容中,星號是 C# 間接運算子 ,用來參考 所 ptr
指向的記憶體內容。 一開始, ptr
指向位圖第一列第一個圖元的第一個字節,但是您可以在變數上 ptr
執行算術,將它移至位圖中的其他位置。
其中一個缺點是,您只能在以 關鍵詞標示unsafe
的程式代碼區塊中使用這個ptr
變數。 此外,元件必須標示為允許不安全的區塊。 這會在項目的屬性中完成。
在 C# 中使用指標非常強大,但也非常危險。 您必須小心,您不會存取記憶體超出指標應該參考的內容。 這就是為什麼指標使用與 「unsafe」 一詞相關聯的原因。
以下是使用 GetPixels
方法之 類別中的 GradientBitmapPage
方法。 請注意使用 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
方法取得時,它會指向位圖第一列最左邊圖元的第一個字節。 和 col
的row
for
循環會設定,ptr
以便在設定每個圖元的每個位元組之後,以 ++
運算符遞增。 對於其他 99 循環的像素, ptr
必須將 設定回位圖的開頭。
每個圖元都是四個字節的記憶體,因此每個位元組都必須個別設定。 這裡的程式代碼假設位元組的順序是紅色、綠色、藍色和Alpha,這與 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
方法來設定,此方法會從紅色、綠色、藍色和Alpha元件建構整數圖元。 請記住, 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);
回想一下IntPtr
,GetPixels
取得參考位圖用來儲存其圖元的記憶體區塊。 呼叫會將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);
}
}
}
若要讓編譯程式優化程序代碼,此頁面是在發行模式中執行。 以下是在 MacBook Pro、Nexus 5 Android 手機和執行 Windows 10 的 Surface Pro 3 上 i 電話 8 模擬器上執行的頁面。 由於硬體差異,請避免比較裝置之間的效能時間,而是查看每個裝置上的相對時間:
以下是以毫秒為單位合併執行時間的數據表:
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 螢幕快照的接下來兩個數據列也相同,這確認這些平台的預設Rgba8888
圖元格式已正確定義小MakePixel
方法。
iOS 和 Android 螢幕快照的底部資料列是向後顯示,這表示透過轉換 SKColor
值取得的不帶正負號整數的格式如下:
AARRGGBB
位元組的順序如下:
BB GG RR AA
這是 Bgra8888
排序,而不是 Rgba8888
排序。 格式 Brga8888
是通用 Windows 平台的預設值,這就是為什麼該螢幕快照最後一列的漸層與第一個數據列相同的原因。 但中間的兩個數據列不正確,因為建立這些點陣圖的程式代碼假設有 Rgba8888
排序。
如果您要使用相同的程式代碼來存取每個平臺上的圖元位,您可以使用 或 Bgra8888
格式明確建立 。SKBitmap
Rgba8888
如果您要將值轉換成 SKColor
點陣圖圖圖元, 請使用 Bgra8888
。
隨機存取圖元
[FillBitmapBytePtr
漸層位圖] 頁面中的 和 FillBitmapUintPtr
方法受益於for
設計為循序填滿位圖的迴圈、從上列到下列,以及從左至右的每個數據列。 圖元可以使用遞增指標的相同語句來設定。
有時候需要隨機存取圖元,而不是循序存取圖元。 如果您使用 GetPixels
方法,則必須根據數據列和數據行來計算指標。 這會在 彩虹正弦圖頁面中示範,這會建立位圖,以正弦 曲線的一個迴圈形式顯示彩虹。
彩虹的顏色是使用 HSL (色調,飽和度,亮度)色彩模型最容易建立的。 方法 SKColor.FromHsl
會使用從 0 到 360 的色調值來建立 SKColor
值(例如圓形的角度,但從紅色、到綠色和藍色,以及回到紅色),以及從 0 到 100 的飽和度和亮度值。 對於彩虹的顏色,飽和度應設定為最大值 100,亮度為 50 的中點。
彩虹正弦 會藉由迴圈查看位圖的數據列,然後迴圈 360 色調值來建立此影像。 從每個色調值,它會根據正弦值來計算點陣圖資料行:
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);
這可讓程式使用將值轉換成SKColor
uint
圖元,而不必擔心。 雖然它不會在這個特定程式中扮演角色,但每當您使用 SKColor
轉換來設定圖元時,您也應該指定 SKAlphaType.Unpremul
,因為 SKColor
不會以 Alpha 值預先假設其色彩元件。
建構函式接著會使用 GetPixels
方法來取得位圖第一個像素的指標:
uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();
對於任何特定的資料列和資料列,必須將位移值加入至 basePtr
。 此位移是點陣圖寬度的數據列時間,加上資料行:
uint* ptr = basePtr + bitmap.Width * row + col;
值 SKColor
會使用此指標儲存在記憶體中:
*ptr = (uint)SKColor.FromHsl(hue, 100, 50);
在的處理程式SKCanvasView
中PaintSurface
,會延展位圖以填滿顯示區域:
從一個點陣圖到另一個點陣圖
許多影像處理工作牽涉到修改圖元,因為它們會從一個位圖傳輸到另一個點陣圖。 這項技術會在 [色彩調整 ] 頁面中示範。 頁面會載入其中一個點陣圖資源,然後可讓您使用三 Slider
個檢視來修改影像:
針對每個圖元色彩,第一 Slider
個將值從 0 新增至 360 到色調,但接著會使用模數運算符將結果保持在 0 到 360 之間,有效地將色彩沿著光譜移動(如 UWP 螢幕快照所示)。 第二 Slider
個可讓您選取 0.5 到 2 之間的乘法因數以套用至飽和度,而第三 Slider
個會針對亮度執行相同動作,如 Android 螢幕快照所示。
程式會維護兩個位陣圖、名為 srcBitmap
的原始來源位圖,以及名為 dstBitmap
的調整目的地位圖。 每次移動 時 Slider
,程式都會計算 中的所有 dstBitmap
新圖元。 當然,使用者會非常快速地移動檢視來 Slider
實驗,因此您想要能管理的最佳效能。 這牽涉到 GetPixels
來源和目的地位圖的方法。
[ 色彩調整 ] 頁面不會控制來源和目的地點圖的色彩格式。 相反地,它包含 與 SKColorType.Bgra8888
格式略有不同的邏輯SKColorType.Rgba8888
。 來源和目的地可以是不同的格式,而且程式仍然可以運作。
以下是程式,除了將圖元形式傳送到目的地的重要 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
而定,位元組會儲存在目的地位元中:
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;
}
}
}
}
···
}
此方法的效能可能會藉由為來源和目的地位圖的各種色彩類型組合建立個別方法,以避免檢查每個圖元的類型,來改善這個方法。 另一個選項是根據色彩類型,為col
變數提供多個for
迴圈。
海報化
另一個涉及存取圖元位的常見工作是 海報化。 如果以點陣圖圖元的色彩會減少,則數位會降低,讓結果與使用有限調色盤的手繪海報類似。
[海報] 頁面會在其中一張猴子影像上執行此程式:
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;
}
}
建構函式中的程式代碼會存取每個圖元、使用值0xE0E0E0FF執行位 AND 作業,然後將結果儲存回點陣圖中。 這些值0xE0E0E0FF會保留每個色彩元件的高 3 位,並將較低的 5 位設定為 0。 位圖會縮減為 224 或 16,777,216 種色彩:29 或 512 種色彩: