為什麼用指標處理圖像?
Bitmap.GetPixel/SetPixel 每次呼叫都有邊界檢查、鎖定/解鎖、格式轉換等開銷,處理大圖極慢。unsafe 程式碼直接操作記憶體,繞過 CLR 托管檢查,效能可提升 10-100 倍。
核心步驟:LockBits 取得記憶體指標
Bitmap bmp = new Bitmap("image.jpg");
// 1. 鎖定位圖記憶體區域
BitmapData data = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb); // 必須指定像素格式
try
{
unsafe
{
// 2. 取得首行掃描線指標
byte* scan0 = (byte*)data.Scan0.ToPointer();
int stride = data.Stride; // 一行實際位元組數 (含記憶體對齊填充)
int width = bmp.Width;
int height = bmp.Height;
// 3. 雙層迴圈逐像素處理
for (int y = 0; y < height; y++)
{
byte* row = scan0 + y * stride; // 當前行起始位址
for (int x = 0; x < width; x++)
{
byte* pixel = row + x * 3; // Format24bppRgb: BGR 順序,每像素 3 位元組
// 範例:灰階轉換 (加權平均法)
byte b = pixel[0];
byte g = pixel[1];
byte r = pixel[2];
byte gray = (byte)((r * 0.299 + g * 0.587 + b * 0.114));
pixel[0] = pixel[1] = pixel[2] = gray;
}
}
}
}
finally
{
// 4. 必須解鎖
bmp.UnlockBits(data);
}
關鍵技術細節
| 概念 | 說明 |
| Stride (跨距) | 一行記憶體位元組數,通常 ≥ Width × BytesPerPixel,因 4 位元組對齊補齊。不可假設 = Width × 3。 |
| Scan0 | 影像資料起始位址 (第一行第一像素)。 |
| 像素格式 | Format24bppRgb 記憶體順序為 B G R (藍綠紅),非 RGB。 |
| 指標運算 | byte* p = scan0 + y * stride + x * 3 定位像素;p[0],p[1],p[2] 存取 BGR。 |
| 專案設定 | 專案屬性 → Build → 勾選 Allow unsafe code。 |
實戰:3x3 中值濾波去雜訊
public void MedianFilter3x3(BitmapData data)
{
unsafe
{
byte* scan0 = (byte*)data.Scan0.ToPointer();
int stride = data.Stride;
int width = data.Width;
int height = data.Height;
byte[] window = new byte[9]; // 3x3 窗口
// 略過邊緣 1 像素
for (int y = 1; y < height - 1; y++)
{
byte* row = scan0 + y * stride;
for (int x = 1; x < width - 1; x++)
{
byte* center = row + x * 3; // 當前像素 B 分量位址
// 收集 3x3 窗口的亮度值 (以綠通道為例,或自行計算灰階)
int idx = 0;
for (int dy = -1; dy <= 1; dy++)
{
byte* r = scan0 + (y + dy) * stride + (x - 1) * 3;
for (int dx = -1; dx <= 1; dx++)
{
window[idx++] = r[dx * 3 + 1]; // 取 G 通道
}
}
// 簡易排序取中值 (9 元素插入排序即可)
for (int i = 1; i < 9; i++)
{
byte val = window[i];
int j = i - 1;
while (j >= 0 && window[j] > val)
window[j + 1] = window[j--];
window[j + 1] = val;
}
byte median = window[4]; // 中位數
// 寫回三通道
center[0] = center[1] = center[2] = median;
}
}
}
}
效能比較參考 (1000x1000 圖片灰階)
| 方法 | 耗時 | 相對速度 |
| GetPixel/SetPixel | ~2500 ms | 1x (基準) |
| LockBits + unsafe 指標 | ~15 ms | ~160x |
| Parallel.For + 指標 | ~5 ms (8 核心) | ~500x |
安全性與最佳實踐
- 最小化 unsafe 範圍:僅將核心運算包在
unsafe { ... } 區塊。
- 檢查像素格式:執行時驗證
bmp.PixelFormat == PixelFormat.Format24bppRgb,否則轉換。
- 邊界處理:濾波器略過邊緣或採用鏡像/複製邊緣策略。
- 記憶體對齊:永遠使用
stride 計算行位移,不要假設緊湊排列。
- 替代方案:.NET 6+ 可考慮
System.Drawing.Common 或 ImageSharp、OpenCvSharp 等專業影像庫。