數位影像本質上是像素矩陣,每個像素包含 R、G、B 三個通道(24bpp 格式)。GDI+ 的 Bitmap 雖不直接暴露像素陣列,但透過 LockBits 取得 BitmapData,配合 unsafe 指標操作,即可實現高效能的逐像素處理。本文以三個經典濾鏡——負片反轉、灰階轉換、亮度調整 為例,完整展示從 UI 到演算法的實作細節。
Form,啟用 AutoScroll 支援大圖瀏覽,覆寫 OnPaint 繪製 Bitmap。public static bool 方法,回傳是否成功,供 UI 決定是否 Invalidate() 重繪。BitmapData bmData = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb); // 強制 24bpp,簡化指標運算
int stride = bmData.Stride; // 一行跨度(含記憶體對齊補齊)
IntPtr scan0 = bmData.Scan0; // 像素資料起始指標
// unsafe 區塊操作...
bmp.UnlockBits(bmData); // 必須釋放
stride 可能大於 width * 3(4 位元組對齊),每行結束需跳過 nOffset = stride - width * 3 位元組。y < height,內層 x < width * 3(逐字節)或 x < width(逐像素,每次 +3 字節)。公式:newVal = 255 - oldVal,對 R、G、B 三通道獨立運算。
public static bool Invert(Bitmap bmp)
{
var bmData = bmp.LockBits(...);
int stride = bmData.Stride;
IntPtr scan0 = bmData.Scan0;
int nOffset = stride - bmp.Width * 3;
unsafe
{
byte* p = (byte*)scan0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width * 3; x++)
{
p[0] = (byte)(255 - p[0]);
p++;
}
p += nOffset;
}
}
bmp.UnlockBits(bmData);
return true;
}
依人眼對 RGB 敏感度差異,採用加權平均:
Gray = 0.299 * R + 0.587 * G + 0.114 * B
注意:GDI+ 記憶體順序為 BGR(非 RGB),讀取時須對應 p[0]=B, p[1]=G, p[2]=R。
public static bool Gray(Bitmap bmp)
{
var bmData = bmp.LockBits(...);
int stride = bmData.Stride;
IntPtr scan0 = bmData.Scan0;
int nOffset = stride - bmp.Width * 3;
unsafe
{
byte* p = (byte*)scan0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
byte blue = p[0];
byte green = p[1];
byte red = p[2];
byte gray = (byte)(0.299 * red + 0.587 * green + 0.114 * blue);
p[0] = p[1] = p[2] = gray;
p += 3;
}
p += nOffset;
}
}
bmp.UnlockBits(bmData);
return true;
}
參數 nBrightness 範圍 [-255, 255],對每通道加上增量並 Clamp(0, 255)。
public static bool Brightness(Bitmap bmp, int nBrightness)
{
if (nBrightness < -255 || nBrightness > 255) return false;
var bmData = bmp.LockBits(...);
int stride = bmData.Stride;
IntPtr scan0 = bmData.Scan0;
int nOffset = stride - bmp.Width * 3;
unsafe
{
byte* p = (byte*)scan0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width * 3; x++)
{
int val = p[0] + nBrightness;
if (val < 0) val = 0;
if (val > 255) val = 255;
p[0] = (byte)val;
p++;
}
p += nOffset;
}
}
bmp.UnlockBits(bmData);
return true;
}
// 開啟影像
private void Open_Click(object sender, EventArgs e)
{
using (var dlg = new OpenFileDialog { Filter = "影像檔|*.bmp;*.jpg;*.png" })
if (dlg.ShowDialog() == DialogResult.OK)
{
_bmp = new Bitmap(dlg.FileName);
AutoScrollMinSize = _bmp.Size;
Invalidate();
}
}
// 儲存影像
private void Save_Click(object sender, EventArgs e)
{
using (var dlg = new SaveFileDialog { Filter = "BMP|*.bmp|JPG|*.jpg" })
if (dlg.ShowDialog() == DialogResult.OK)
_bmp.Save(dlg.FileName);
}
// 負片
private void Invert_Click(object sender, EventArgs e)
{
if (Filters.Invert(_bmp)) Invalidate();
}
// 灰階
private void Gray_Click(object sender, EventArgs e)
{
if (Filters.Gray(_bmp)) Invalidate();
}
// 亮度(彈出輸入對話框)
private void Brightness_Click(object sender, EventArgs e)
{
using (var dlg = new ParameterForm()) // 自訂簡單 Form,含 NumericUpDown
if (dlg.ShowDialog() == DialogResult.OK)
if (Filters.Brightness(_bmp, dlg.Value)) Invalidate();
}
System.Drawing.Common(.NET Core / .NET 5+ 需額外安裝 NuGet 套件)。GC.TryStartNoGCRegion 或分塊處理,避免大物件堆積(LOH)壓力。newVal = (oldVal - 128) * contrast + 128。PixelFormat.Format32bppArgb,處理 Alpha 通道。Filters 改為泛型 IImageFilter 介面,配合策略模式動態載入濾鏡外掛。掌握 LockBits + unsafe 指標操作,即可在 C# 中實現媲美 C++ 的影像處理效能,為電腦視覺、文件掃描、醫療影像等應用打下堅實基礎。