首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 將 byte 陣列轉為 8 位元灰階圖像:高效能 LockBits 實作詳解

     編輯:關於C#

在影像處理應用中,將原始位元組陣列轉換為可顯示的 Bitmap 是常見需求。網路上雖有許多範例,但常忽略關鍵細節導致效能低落或色彩錯誤。本文詳細解析 8bpp 灰階圖像轉換的完整流程,包含記憶體對齊、調色板修正、高效像素存取等核心技巧。

核心概念與前置知識

  • Byte 陣列對應像素:每個元素 0~255 代表一個像素灰階值
  • 需知寬高:單純陣列無法還原圖像,必須知道 Width/Height
  • PixelFormat 必須為 Format8bppIndexed:符合 8 位元索引色彩特性
  • 避免 GetPixel/SetPixel:效能極差,大量像素操作必須用 LockBits
  • Stride 與 Width 不同:掃描列寬度為 4 的倍數,記憶體實際大小 ≠ 顯示大小
  • 調色板預設為偽彩:必須手動修改為灰階調色板

完整轉換程式碼


/// 
/// 將 byte 陣列轉換為 8bpp 灰階 Bitmap
/// 
public static Bitmap ToGrayBitmap(byte[] rawValues, int width, int height)
{
    // 1. 建立目標 Bitmap 並鎖定記憶體
    Bitmap bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
    BitmapData bmpData = bmp.LockBits(
        new Rectangle(0, 0, width, height),
        ImageLockMode.WriteOnly,
        PixelFormat.Format8bppIndexed);

    int stride = bmpData.Stride;          // 掃描列寬度(4 的倍數)
    int offset = stride - width;          // 每列結尾的填充位元組數
    IntPtr scan0 = bmpData.Scan0;         // 記憶體起始位址
    int scanBytes = stride * height;      // 實際記憶體總大小

    // 2. 將邏輯像素陣列轉為含 padding 的實體陣列
    byte[] pixelValues = new byte[scanBytes];
    int posScan = 0, posReal = 0;

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
            pixelValues[posScan++] = rawValues[posReal++];
        posScan += offset; // 跳過 padding
    }

    // 3. 使用 Marshal.Copy 高效複製到非受控記憶體
    Marshal.Copy(pixelValues, 0, scan0, scanBytes);
    bmp.UnlockBits(bmpData);

    // 4. 修正調色板:將偽彩改為真灰階
    ColorPalette palette;
    using (Bitmap tmp = new Bitmap(1, 1, PixelFormat.Format8bppIndexed))
        palette = tmp.Palette;

    for (int i = 0; i < 256; i++)
        palette.Entries[i] = Color.FromArgb(i, i, i); // R=G=B 即灰階

    bmp.Palette = palette;
    return bmp;
}

關鍵步驟解析

LockBits / UnlockBits 機制

LockBits 將圖像資料鎖定在記憶體中,回傳 BitmapData 提供 Scan0(起始指標)與 Stride(掃描列寬度)。這讓我們能像操作陣列一樣直接存取像素,避免 GetPixel/SetPixel 的邊界檢查與格式轉換開銷。

Stride 與 Offset 處理

位圖掃描列必須對齊 4 位元組邊界。例如寬度 100 的圖像,Stride 會是 100(剛好是 4 的倍數);但寬度 101 時,Stride 會變成 104。複製像素時,每列結尾需跳過 offset = stride - width 個位元組,否則影像會傾斜撕裂。

Marshal.Copy 批次複製

先在記憶體中組裝好含 padding 的完整陣列,再一次 Marshal.Copy 複製到非受控記憶體,比逐像素寫入快數個數量級。

調色板修正

Format8bppIndexed 使用索引色彩,預設調色板為偽彩色。建立 1x1 臨時圖像取得空白調色板,將 256 個索引對應至 Color.FromArgb(i,i,i),即可得到標準灰階漸層。

測試程式碼與結果


static void Main()
{
    byte[] bytes = new byte[10000];
    int k = 0;
    for (int i = 0; i < 100; i++)
        for (int j = 0; j < 100; j++)
            bytes[k++] = (byte)(i + j); // 產生對角線漸層

    Bitmap bmp = ToGrayBitmap(bytes, 100, 100);
    bmp.Save(@"d:\test.png", ImageFormat.Png);
}

執行結果會產生平滑的對角線灰階漸層圖。若跳過調色板修正步驟,輸出將呈現彩虹色偽彩效果。

效能比較與最佳實務

方法100x100 耗時1000x1000 耗時備註
GetPixel/SetPixel~120 ms~12 秒極慢,不建議生產使用
LockBits + Marshal.Copy<1 ms~15 ms推薦做法
unsafe 指標操作<1 ms~10 ms稍快但需 unsafe context

常見坑點提醒

  1. 忘記 UnlockBits:會導致記憶體洩漏與 GDI+ 物件鎖死
  2. Stride 計算錯誤:影像斜裂、最後幾列亂碼
  3. 調色板未修正:灰階圖變成偽彩色
  4. 陣列長度不足:scanBytes = stride * height,非 width * height

擴充應用

此技術廣泛應用於:醫療影像(DICOM)、工業檢測、即時影像處理、熱像儀資料視覺化等場景。搭配 Parallel.For 可進一步平行化多影像批次轉換。

總結

掌握 LockBits、Stride 對齊、Marshal.Copy 批次複製、調色板修正四大關鍵,即能高效、正確地將原始位元組陣列轉換為標準灰階 Bitmap,為後續影像分析、顯示、儲存奠定堅實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved