Share via


存取 SkiaSharp 位圖圖圖元位

如將 SkiaSharp 位圖儲存至檔案一文中所見,位圖通常會以壓縮格式儲存在檔案中,例如 JPEG 或 PNG。 在 constrast 中,不會壓縮儲存在記憶體中的 SkiaSharp 位陣陣圖。 它會儲存為循序數列的圖元。 這個未壓縮的格式有助於將點陣圖傳輸至顯示介面。

SkiaSharp 位陣圖所佔用的記憶體區塊會以非常直接的方式組織:它會從左到右的第一個像素數據列開始,然後繼續進行第二個數據列。 對於全色位圖,每個圖元都包含四個字節,這表示位圖所需的總記憶體空間是其寬度和高度的四倍。

本文說明應用程式如何透過存取位圖的圖元記憶體區塊,或間接存取這些圖元。 在某些情況下,程式可能會想要分析影像的圖元,並建構某種類型的直方圖。 更常見的是,應用程式可以藉由以演算法方式建立構成位圖的圖元來建構唯一影像:

圖元位範例

技術

SkiaSharp 提供數種技術來存取位圖的圖元。 您選擇的通常是程式代碼撰寫便利性(與維護和偵錯容易相關)和效能之間的妥協。 在大部分情況下,您將使用 下列其中一種方法和屬性 SKBitmap 來存取位圖的圖元:

  • GetPixelSetPixel 方法可讓您取得或設定單一像素的色彩。
  • 屬性 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

以下是 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 索引必須從 rowcol 變數計算。 數據列會乘以每個數據列的像素數(在此案例中為 256),然後加入數據行。

SKBitmap 也會定義類似的 Bytes 屬性,這個屬性會傳回整個位圖的位元組陣列,但對全色位圖來說比較麻煩。

GetPixels 指標

存取位圖圖元的最強大技術可能是 GetPixels,不會與 GetPixel 方法或 Pixels 屬性混淆。 您會立即注意到 GetPixels ,其會傳回 C# 程式設計中不太常見的內容:

IntPtr pixelsAddr = bitmap.GetPixels();

.NET IntPtr 類型代表指標。 IntPtr因為它是執行程式之機器之原生處理器的整數長度,通常為32位或64位長度。 IntPtrGetPixels回的 是位圖對象用來儲存其像素的實際記憶體區塊位址。

您可以使用 方法,將轉換成 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 方法取得時,它會指向位圖第一列最左邊圖元的第一個字節。 和 colrowfor循環會設定,ptr以便在設定每個圖元的每個位元組之後,以 ++ 運算符遞增。 對於其他 99 循環的像素, ptr 必須將 設定回位圖的開頭。

每個圖元都是四個字節的記憶體,因此每個位元組都必須個別設定。 這裡的程式代碼假設位元組的順序是紅色、綠色、藍色和Alpha,這與 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 方法來設定,此方法會從紅色、綠色、藍色和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);

回想一下IntPtrGetPixels取得參考位圖用來儲存其圖元的記憶體區塊。 呼叫會將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 屬性會更好,甚至與一些 GetPixelsSetPixels 技術比較良好。 使用 uint 圖元值通常比設定個別 byte 元件還要快,而將值轉換成 SKColor 不帶正負號的整數會增加程式的額外負荷。

比較各種漸層也很有趣:每個平臺的頂端數據列都相同,並如預期般顯示漸層。 這表示 SetPixel 方法與 Pixels 屬性會正確從色彩建立圖元,而不論基礎圖元格式為何。

iOS 和 Android 螢幕快照的接下來兩個數據列也相同,這確認這些平台的預設Rgba8888圖元格式已正確定義小MakePixel方法。

iOS 和 Android 螢幕快照的底部資料列是向後顯示,這表示透過轉換 SKColor 值取得的不帶正負號整數的格式如下:

AARRGGBB

位元組的順序如下:

BB GG RR AA

這是 Bgra8888 排序,而不是 Rgba8888 排序。 格式 Brga8888 是通用 Windows 平台的預設值,這就是為什麼該螢幕快照最後一列的漸層與第一個數據列相同的原因。 但中間的兩個數據列不正確,因為建立這些點陣圖的程式代碼假設有 Rgba8888 排序。

如果您要使用相同的程式代碼來存取每個平臺上的圖元位,您可以使用 或 Bgra8888 格式明確建立 。SKBitmapRgba8888 如果您要將值轉換成 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);

這可讓程式使用將值轉換成SKColoruint圖元,而不必擔心。 雖然它不會在這個特定程式中扮演角色,但每當您使用 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);

在的處理程式SKCanvasViewPaintSurface,會延展位圖以填滿顯示區域:

彩虹正弦

從一個點陣圖到另一個點陣圖

許多影像處理工作牽涉到修改圖元,因為它們會從一個位圖傳輸到另一個點陣圖。 這項技術會在 [色彩調整 ] 頁面中示範。 頁面會載入其中一個點陣圖資源,然後可讓您使用三 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);
    }
}

檢視 ValueChangedSlider 處理程式會計算調整值並呼叫 TransferPixels

整個 TransferPixels 方法都會標示為 unsafe。 其一開始是取得兩個位圖圖圖元位的位元組指標,然後迴圈查看所有數據列和數據行。 從來源位圖中,方法會為每個圖元取得四個字節。 這些可以是 Rgba8888Bgra8888 順序。 檢查色彩類型可建立 SKColor 值。 接著會擷取、調整 HSL 元件,並用來重新建立 SKColor 值。 根據目的地位圖為 Rgba8888Bgra8888而定,位元組會儲存在目的地位元中:

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 種色彩:

螢幕快照顯示兩個行動裝置和桌面視窗上玩具猴子的海報影像。